| perf-test/clj/reitit | ||
| scripts | ||
| src/reitit | ||
| test | ||
| .gitignore | ||
| .travis.yml | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| project.clj | ||
| README.md | ||
reitit

A friendly data-driven router for Clojure(Script).
- Simple data-driven route syntax
- First-class route meta-data
- Generic, not tied to HTTP
- Route conflict resolution
- Pluggable coercion (clojure.spec)
- Middleware & Interceptors
- Extendable
- Fast
Ships with example router for Ring. See Issues for roadmap.
Latest version
Route Syntax
Routes are defined as vectors, which String path, optional (non-vector) route argument and optional child routes. Routes can be wrapped in vectors.
Simple route:
["/ping"]
Two routes:
[["/ping"]
["/pong"]]
Routes with meta-data:
[["/ping" ::ping]
["/pong" {:name ::pong}]]
Routes with path and catch-all parameters:
[["/users/:user-id"]
["/public/*path"]]
Nested routes with meta-data:
["/api"
["/admin" {:middleware [::admin]}
["/user" ::user]
["/db" ::db]
["/ping" ::ping]]
Same routes flattened:
[["/api/admin/user" {:middleware [::admin], :name ::user}
["/api/admin/db" {:middleware [::admin], :name ::db}
["/api/ping" ::ping]]
Routing
For routing, a Router is needed. Reitit ships with several different router implementations: :linear-router, :lookup-router and :mixed-router, based on the awesome Pedestal implementation.
Router is created with reitit.core/router, which takes routes and optional options map as arguments. The route tree gets expanded, optionally coerced and compiled. Actual Router implementation is selected automatically but can be defined with a :router option. Router support both path- and name-based lookups.
Creating a router:
(require '[reitit.core :as reitit])
(def router
(reitit/router
[["/api"
["/ping" ::ping]
["/user/:id" ::user]]]))
:mixed-router is created (both static & wild routes are found):
(reitit/router-type router)
; :mixed-router
The expanded routes:
(reitit/routes router)
; [["/api/ping" {:name :user/ping}]
; ["/api/user/:id" {:name :user/user}]]
Route names:
(reitit/route-names router)
; [:user/ping :user/user]
Path-based routing
(reitit/match-by-path router "/hello")
; nil
(reitit/match-by-path router "/api/user/1")
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
; :path "/api/user/1"
; :result nil
; :params {:id "1"}}
Name-based (reverse) routing
(reitit/match-by-name router ::user)
; #PartialMatch{:template "/api/user/:id",
; :meta {:name :user/user},
; :result nil,
; :params nil,
; :required #{:id}}
(reitit/partial-match? (reitit/match-by-name router ::user))
; true
Only a partial match. Let's provide the path-parameters:
(reitit/match-by-name router ::user {:id "1"})
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
; :path "/api/user/1"
; :result nil
; :params {:id "1"}}
There is also a exception throwing version:
(reitit/match-by-name! router ::user)
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
Route meta-data
Routes can have arbitrary meta-data. For nested routes, the meta-data is accumulated from root towards leafs using meta-merge.
A router based on nested route tree:
(reitit/router
["/api" {:interceptors [::api]}
["/ping" ::ping]
["/admin" {:roles #{:admin}}
["/users" ::users]
["/db" {:interceptors [::db], :roles ^:replace #{:db-admin}}
["/:db" {:parameters {:db String}}
["/drop" ::drop-db]
["/stats" ::db-stats]]]]]))
Resolved route tree:
(reitit/routes router)
; [["/api/ping" {:interceptors [::api]
; :name ::ping}]
; ["/api/admin/users" {:interceptors [::api]
; :roles #{:admin}
; :name ::users}]
; ["/api/admin/db/:db/drop" {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::drop-db}]
; ["/api/admin/db/:db/stats" {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::db-stats}]]
Path-based routing:
(reitit/match-by-path router "/api/admin/users")
; #Match{:template "/api/admin/users"
; :meta {:interceptors [::api]
; :roles #{:admin}
; :name ::users}
; :result nil
; :params {}
; :path "/api/admin/users"}
On match, route meta-data is returned and can interpreted by the application.
Routers also support meta-data compilation enabling things like fast Ring or Pedestal -style handlers. Compilation results are found under :result in the match. See configuring routers for details.
Route conflicts
Route trees should not have multiple routes that match to a single (request) path. router checks the route tree at creation for conflicts and calls a registered :conflicts option callback with the found conflicts. Default implementation throws ex-info with a descriptive message.
(reitit/router
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]])
; CompilerException clojure.lang.ExceptionInfo: router contains conflicting routes:
;
; /:user-id/orders
; -> /public/*path
; -> /bulk/:bulk-id
;
; /bulk/:bulk-id
; -> /:version/status
;
; /public/*path
; -> /:version/status
;
Ring
Ring-router adds support for handlers, middleware and routing based on :request-method.
Simple Ring app:
(require '[reitit.ring :as ring])
(defn handler [_]
{:status 200, :body "ok"})
(def app
(ring/ring-handler
(ring/router
["/ping" handler])))
The expanded routes:
(-> app (ring/get-router) (reitit/routes))
; [["/ping" {:handler #object[...]}]]
Applying the handler:
(app {:request-method :get, :uri "/favicon.ico"})
; nil
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
Request-method based routing
(def app
(ring/ring-handler
(ring/router
["/ping" {:name ::ping
:get handler
:post handler}])))
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
(app {:request-method :put, :uri "/ping"})
; nil
Reverse routing:
(-> app
(ring/get-router)
(reitit/match-by-name ::ping)
:path)
; "/ping"
Middleware
Let's define some middleware and a handler:
(defn wrap [handler id]
(fn [request]
(handler (update request ::acc (fnil conj []) id))))
(defn wrap-api [handler]
(wrap handler :api))
(defn handler [{:keys [::acc]}]
{:status 200, :body (conj acc :handler)})
App with nested middleware:
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [wrap-api]}
["/ping" handler]
["/admin" {:middleware [[wrap :admin]]}
["/db" {:middleware [[wrap :db]]
:delete {:middleware [#(wrap % :delete)]
:handler handler}}]]])))
Middleware is applied correctly:
(app {:request-method :delete, :uri "/api/ping"})
; {:status 200, :body [:api :handler]}
(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}
Async Ring
All built-in middleware provide both the 2 and 3-arity, so they work with Async Ring too.
Meta-data based extensions
The routing Match is injected into a request and can be extracted with reitit.ring/get-match. It can be used to build dynamic extensions to the system.
A middleware to guard routes:
(require '[clojure.set :as set])
(defn wrap-enforce-roles [handler]
(fn [{:keys [::roles] :as request}]
(let [required (some-> request (ring/get-match) :meta ::roles)]
(if (and (seq required) (not (set/intersection required roles)))
{:status 403, :body "forbidden"}
(handler request)))))
Mounted to an app via router meta-data (effecting all routes):
(def handler (constantly {:status 200, :body "ok"}))
(def app
(ring/ring-handler
(ring/router
[["/api"
["/ping" handler]
["/admin" {::roles #{:admin}}
["/ping" handler]]]]
{:meta {:middleware [wrap-enforce-roles]}})))
Anonymous access to public route:
(app {:request-method :get, :uri "/api/ping"})
; {:status 200, :body "ok"}
Anonymous access to guarded route:
(app {:request-method :get, :uri "/api/admin/ping"})
; {:status 403, :body "forbidden"}
Authorized access to guarded route:
(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}})
; {:status 200, :body "ok"}
Parameter coercion
Reitit ships with pluggable parameter coercion via reitit.coercion.protocol/Coercion protocol. reitit.coercion.spec/SpecCoercion provides implements it for clojure.spec & data-specs.
NOTE: to use the spec-coercion, one needs to add the following dependencies manually to the project:
[org.clojure/clojure "1.9.0-alpha17"]
[org.clojure/spec.alpha "0.1.123"]
[metosin/spec-tools "0.3.2"]
Ring request and response coercion
To use Coercion with Ring, one needs to do the following:
- Define parameters and responses as data into route meta-data, in format adopted from ring-swagger:
:parametersmap, with submaps for different parameters::query,:body,:form,:headerand:path. Parameters are defined in the format understood by theCoercion.:responsesmap, with response status codes as keys (or:defaultfor "everything else") with maps with:schemaand optionally:descriptionas values.
- Define a
Coercionto route meta-data under:coercion - Mount request & response coercion middleware to the routes.
If the request coercion succeeds, the coerced parameters are injected into request under :parameters.
If either request or response coercion fails, an descriptive error is thrown.
Example with data-specs
(require '[reitit.ring :as ring])
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.spec :as spec])
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:parameters {:body {:x int?, :y int?}}
:responses {200 {:schema {:total pos-int?}}}
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
{:meta {:middleware [coercion/gen-wrap-coerce-parameters
coercion/gen-wrap-coerce-response]
:coercion spec/coercion}})))
(app
{:request-method :get
:uri "/api/ping"
:body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}
Example with specs
(require '[reitit.ring :as ring])
(require '[reitit.coercion :as coercion])
(require '[reitit.coercion.spec :as spec])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])
(s/def ::x (st/spec int?))
(s/def ::y (st/spec int?))
(s/def ::total int?)
(s/def ::request (s/keys :req-un [::x ::y]))
(s/def ::response (s/keys :req-un [::total]))
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" {:parameters {:body ::request}
:responses {200 {:schema ::response}}
:get {:handler (fn [{{{:keys [x y]} :body} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]]
{:meta {:middleware [coercion/gen-wrap-coerce-parameters
coercion/gen-wrap-coerce-response]
:coercion spec/coercion}})))
(app
{:request-method :get
:uri "/api/ping"
:body-params {:x 1, :y 2}})
; {:status 200, :body {:total 3}}
Compiling Middleware
The meta-data extensions are a easy way to extend the system. Routes meta-data can be trasnformed into any shape (records, functions etc.) in route compilation, enabling easy access at request-time.
Still, we can do better. As we know the exact route interceptor/middleware is linked to, we can pass the (compiled) route information into the interceptor/middleware at creation-time. It can extract and transform relevant data just for it and pass it into the actual request-handler via a closure. We can do all the static local computations forehand, yielding much lighter runtime processing.
For middleware, there is a helper reitit.middleware/gen for this. It takes a function of route-meta router-opts => middleware and returns a special record extending the internal middleware protocols so it can be mounted as normal middleware. The compiled middleware can also decide no to mount itsef byt returning nil. Why mount wrap-enforce-roles if there are no roles required for that route?
To demonstrate the two approaches, below are response coercion middleware written in both ways (found in reitit.coercion):
Naive
- Extracts the compiled route information on every request.
(defn wrap-coerce-response
"Pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responeses from route meta, otherwise does not mount."
[handler]
(fn
([request]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(coerce-response coercers request (handler request)))
(handler request))))
([request respond raise]
(let [response (handler request)
method (:request-method request)
match (ring/get-match request)
responses (-> match :result method :meta :responses)
coercion (-> match :meta :coercion)
opts (-> match :meta :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)
coerced (coerce-response coercers request response)]
(handler request #(respond (coerce-response coercers request %))))
(handler request respond raise))))))
Compiled
- Route information is provided via a closure
- Pre-compiled coercers
- Mounts only if
:coercionand:responsesare defined for the route
(def gen-wrap-coerce-response
"Generator for pluggable response coercion middleware.
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
and :responses from route meta, otherwise does not mount."
(middleware/gen
(fn [{:keys [responses coercion opts]} _]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(fn [handler]
(fn
([request]
(coerce-response coercers request (handler request)))
([request respond raise]
(handler request #(respond (coerce-response coercers request %)) raise)))))))))
Merging route-trees
TODO
Validating meta-data
TODO
Swagger & Openapi
TODO
Interceptors
TODO
Configuring Routers
Routers can be configured via options. Options allow things like clojure.spec validation for meta-data and fast, compiled handlers. The following options are available for the reitit.core/router:
| key | description |
|---|---|
:path |
Base-path for routes (default "") |
:routes |
Initial resolved routes (default []) |
:meta |
Initial expanded route-meta vector (default []) |
:expand |
Function of arg opts => meta to expand route arg to route meta-data (default reitit.core/expand) |
:coerce |
Function of route opts => route to coerce resolved route, can throw or return nil |
:compile |
Function of route opts => result to compile a route handler |
:conflicts |
Function of {route #{route}} => side-effect to handle conflicting routes (default reitit.core/throw-on-conflicts!) |
:router |
Function of routes opts => router to override the actual router implementation |
Special thanks
To all Clojure(Script) routing libs out there, expecially to Ataraxy, Bide, Bidi, Compojure and Pedestal.
License
Copyright © 2017 Metosin Oy
Distributed under the Eclipse Public License, the same as Clojure.