Data-driven Middleware
Ring defines middleware as a function of type handler & args => request => response
. It is relatively easy to understand and allows for good performance. A downside is that the middleware chain is just a opaque function, making things like debugging and composition hard. It is too easy to apply the middlewares in wrong order.
Reitit defines middleware as data:
- A middleware can be defined as first-class data entries
- A middleware can be mounted as a duct-style vector (of middlewares)
- A middleware can be optimized & compiled against an endpoint
- A middleware chain can be transformed by the router
Middleware as data
All values in the :middleware
vector of route data are expanded into reitit.middleware/Middleware
Records by using the reitit.middleware/IntoMiddleware
Protocol. By default, functions, maps and Middleware
records are allowed.
Records can have arbitrary keys, but the following keys have special purpose:
key | description |
---|---|
:name | Name of the middleware as a qualified keyword |
:spec | clojure.spec definition for the route data, see route data validation (optional) |
:wrap | The actual middleware function of handler & args => request => response |
:compile | Middleware compilation function, see compiling middleware. |
Middleware Records are accessible in their raw form in the compiled route results, and thus are available for inventories, creating api-docs, etc.
For the actual request processing, the Records are unwrapped into normal functions and composed into a middleware function chain, yielding zero runtime penalty.
Creating Middleware
The following examples produce identical middleware runtime functions.
Function
(defn wrap [handler id]
(fn [request]
(handler (update request ::acc (fnil conj []) id))))
Map
(def wrap3
{:name ::wrap3
:description "Middleware that does things."
:wrap wrap})
Record
(require '[reitit.middleware :as middleware])
(def wrap2
(middleware/map->Middleware
{:name ::wrap2
:description "Middleware that does things."
:wrap wrap}))
Using Middleware
:middleware
is merged to endpoints by the router
.
(require '[reitit.ring :as ring])
(defn handler [{::keys [acc]}]
{:status 200, :body (conj acc :handler)})
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [[wrap 1] [wrap2 2]]}
["/ping" {:get {:middleware [[wrap3 3]]
:handler handler}}]])))
All the middlewares are applied correctly:
(app {:request-method :get, :uri "/api/ping"})
; {:status 200, :body [1 2 3 :handler]}
Compiling middleware
Middlewares can be optimized against an endpoint using middleware compilation.
Ideas for the future
- Support Middleware dependency resolution with new keys
:requires
and:provides
. Values are set of top-level keys of the request. e.g.InjectUserIntoRequestMiddleware
requires#{:session}
and provides#{:user}
AuthorizationMiddleware
requires#{:user}
Ideas welcome & see issues for details.