Skip to content

untangled-web/untangled-server

Repository files navigation

Untangled Server

The Untangled server library provides a full-stack development experience for Untangled Web applications. When combined with the client library, you get a number of base features that are useful for most applications:

  • An easy-to-use pluggable architecture for adding in databases and other components

  • A clear way to add read/write semantics for handling Untangled queries and mutations

  • Processing pipeline hooks for pre-processing, post-processing, and non-API routes (e.g. file serving)

  • General data compression and file resource cache headers

The overall network plumbing of Untangled includes a number of additional features that assist with some common patterns needed by most applications:

  • JSON+Transit for API encoding

  • The ability to elide UI-only bits of query when using a general UI query against a server

  • A clean story for app-state merging that includes attribute "stomping" semantics

  • Clear network communication ordering to prevent out-of-order execution reasoning

  • The ability to send queries in parallel when sequential reads are not necessary for clear reasoning (parallel loading)

  • A pluggable ring handler middleware stack for injecting your own middleware as needed.

  • Provides access to the underlying stuartsierra component system for injecting your own components.

  • Lets you write your own api routes using a thin wrapper around bidi

  • Configurable configuration component that supports: a defaults.edn file, a path to a config edn file for merging into the defaults, and support for environmental variable access.

(ns your.system
  (:require
    [com.stuartsierra.component :as cp]
    [untangled.server.core :as usc]
    [om.next.server :as oms]))

(def your-server
  (usc/make-untangled-server
    :config-path "/your/config/path.edn" ;;(1)
    :components {:your-key (your-component)} ;;(2)
    :parser (oms/parser {:read your-read :mutate your-mutate}) ;;(3)
    :parser-injections #{:config :your-key})) ;;(4)

(cp/start your-server) ;;(5)
  1. Optional path to a edn config file that will override any defaults found in config/defaults.edn.

  2. A map for your components, eg: databases, custom networking, etc…​

  3. A parser to parse untangled-client (ie om.next) reads and mutates

  4. A set of keywords corresponding to component keys that will be injected into the parser environment. :config is a special case that untangled-server always creates one of.

  5. Simply start the returned system. This can be at the top level, or inside some other function that you control (eg: -main).

untangled.server.core/untangled-system is the recommended way to build untangled servers.
The basic concepts and differences between it and The Easy Way are as follows:

  1. It does less work and creates fewer implicit components behind the covers,
    this so you (the user) have more control and flexibility over what your untangled-server does and provides.

  2. You control your server, and if you are using ring with it, your own middleware stack.

  3. Provides an api-handler with a ring middleware function that takes care of parsing requests from an untangled client.

  4. You control the composition of parsing functions (reads and mutates) from any number of sources.
    This is invaluable when trying to consume libraries that provide parser functions,
    but must be injected into the api-handler in a very specific order (or just for performance reasons).

  5. Module s are what untangled-server calls the components that provide components,
    and if they implement APIHandler, they also provide parser functions (api-read and api-write).

  6. An APIHandler satisfying component should depend on any other components it needs for parsing,
    as they will get put in its parsing environment. This obsoletes the old :parser-injections method
    by superceding it with a dependency injection system that limits the injection to just that APIHandler component.

  7. You control where the api-handler gets located in the returned system.
    For example, you would use this to extract the api-handler (which parses reads and mutations from a request) into a java servlet.

The following examples all rely on these requires:

(ns your.system
  (:require
    [com.stuartsierra.component :as cp]
    [untangled.server.core :as usc]
    [untangled.server.impl.middleware :as mid]))
(defn MIDDLEWARE [handler component] (5)
  ((get component :middleware) handler))

(defrecord YourRingHandler [api-handler]
  cp/Lifecycle
  (start [this]
    (assoc this :middleware ;;(2)
      (-> (fn [req] {:status 404}) ;;(3)(4)
        #_...
        (MIDDLEWARE api-handler) ;;(6)
        (mid/wrap-transit-params) ;;(7)
        (mid/wrap-transit-response) ;;(7)
        #_...)))
  (stop [this] (dissoc this :middleware)))

(defn make-your-ring-handler [api-handler-key]
  (cp/using (->YourRingHandler) {:api-handler api-handler-key}) ;;(1)
  1. Depend on the api-handler as api-handler.

  2. Assoc a middleware function under :middleware.

  3. A middleware function takes a request and returns a response.

  4. A simple not-found handler for showing the signature of the middleware.

  5. A small utility function for being able to compose middleware components in a threading arrow.

  6. Install the api-handler middleware into the location of choosing.

  7. Add the transit middleware for encoding/decoding parameters and responses.

Warning
The transit middleware is required when dealing with an untangled-client with the default transit based networking.
(def your-server
  (usc/untangled-system
    {:api-handler-key ::your-api-handler-key ;;(1)
     :components {:config (usc/new-config) ;;(2)
                  :server (usc/make-web-server ::handler) ;;(3)
                  ::handler (make-your-ring-handler ::your-api-handler-key)}})) ;;(4)

;; EXAMPLE USAGE
(cp/start your-server) ;;(5)

(.start some-java-servlet (::your-api-handler-key (cp/start your-server))) ;;(5)
  1. You can redifine where the api-handler is located, defaults to ::usc/api-handler

  2. You are responsible for creating whatever config you need.

  3. The web-server we provide takes an optional keyword that points to the handler component key it should depend on and look inside of for a :middleware fn.

  4. We create a ring handler as described earlier with the api-handler-key as a dependency.

  5. You can just start the system, or embed it in some other container that deals with serving requests, eg: some java servlet.

(defrecord YourApiModule []
  usc/Module
  (system-key [this] ::YourApiModule) ;;(2)
  (components [this] {#_..sub-components..}) ;;(3)
  usc/APIHandler
  (api-read [this]
    (fn [{:as env :keys [db]} k params] #_...)) ;;(4)(5)
  (api-mutate [this]
    (fn [{:as env :keys [db]} k params] #_...)) ;;(4)(5)
(defn make-your-api-module []
  (cp/using (->YourApiModule) [:db #_..sub-components..])) ;;(3)(5)

(def your-server
  (usc/untangled-system
    {:components {#_...}
     :modules [(make-your-api-module) #_...]})) ;;(1)
  1. You can have any number of modules, they compose left to right (ie: they are tried in that order).

  2. Modules must have a unique system-key.

  3. Modules can also have uniquely named sub components, but must at minimum be implemented to return nil or {}.

  4. Modules that implement usc/APIHandler must implement both api-read and api-mutate to return an appropriate parser function. These functions can however return nil at any time to indicate to the api parsing plumbing that it does not know how to respond, and that the next module should attempt to respond.

  5. To use a component in your parser environments (env), make the component depend on it using cp/using.

The MIT License (MIT) Copyright © 2016 NAVIS