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

Snappy data-driven router for Clojure(Script).
- Simple data-driven route syntax
- First-class route meta-data
- Generic, not tied to HTTP
- Extendable
- Fast
Latest version
Route Syntax
Routes are defined as vectors, which String path as the first element, then optional meta-data (non-vector) 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]]
Previous example flattened:
[["/api/admin/user" {:middleware [::admin], :name ::user}
["/api/admin/db" {:middleware [::admin], :name ::db}
["/api/ping" ::ping]]
Routers
For actual routing, we need to create a Router. Reitit ships with 2 different router implementations: LinearRouter and LookupRouter, both based on the awesome Pedestal implementation.
Router is created with reitit.core/router, which takes routes and optionally an options map as arguments. The route-tree gets expanded, optionally coerced and compiled to support both fast path- and name-based lookups.
Creating a router:
(require '[reitit.core :as reitit])
(def router
(reitit/router
[["/api"
["/ping" ::ping]
["/user/:id" ::user]]))
LinearRouter is created (as there are wildcard):
(class router)
; reitit.core.LinearRouter
The expanded routes:
(reitit/routes router)
; [["/api/ping" {:name :user/ping}]
; ["/api/user/:id" {:name :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"
; :handler nil
; :params {:id "1"}}
Name-based (reverse) routing:
(reitit/match-by-name router ::user)
; ExceptionInfo missing path-params for route '/api/user/:id': #{:id}
(reitit/match-by-name router ::user {:id "1"})
; #Match{:template "/api/user/:id"
; :meta {:name :user/user}
; :path "/api/user/1"
; :handler nil
; :params {:id "1"}}
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:
(def router
(reitit/router
["/api" {:interceptors [::api]}
["/ping" ::ping]
["/public/*path" ::resources]
["/user/:id" {:name ::get-user
:parameters {:id String}}
["/orders" ::user-orders]]
["/admin" {:interceptors [::admin]
:roles #{:admin}}
["/root" {:name ::root
:roles ^:replace #{:root}}]
["/db" {:name ::db
:interceptors [::db]}]]]))
Resolved route tree:
(reitit/routes router)
; [["/api/ping" {:name :user/ping
; :interceptors [::api]}]
; ["/api/public/*path" {:name :user/resources
; :interceptors [::api]}]
; ["/api/user/:id/orders" {:name :user/user-orders
; :interceptors [::api]
; :parameters {:id String}}]
; ["/api/admin/root" {:name :user/root
; :interceptors [::api ::admin]
; :roles #{:root}}]
; ["/api/admin/db" {:name :user/db
; :interceptors [::api ::admin ::db]
; :roles #{:admin}}]]
Path-based routing:
(reitit/match-by-path router "/api/admin/root")
; #Match{:template "/api/admin/root"
; :meta {:name :user/root
; :interceptors [::api ::admin]
; :roles #{:root}}
; :path "/api/admin/root"
; :handler nil
; :params {}}
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 :handler in the match. See configuring routers for details.
Ring
Simple Ring-based routing app:
(require '[reitit.ring :as ring])
(defn handler [_]
{:status 200, :body "ok"})
(def app
(ring/ring-handler
(ring/router
["/ping" handler])))
It's backed by a LookupRouter (no wildcards!)
(-> app (ring/get-router) class)
; reitit.core.LookupRouter
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"}
Routing based on :request-method:
(def app
(ring/ring-handler
(ring/router
["/ping" {:get handler
:post handler}])))
(app {:request-method :get, :uri "/ping"})
; {:status 200, :body "ok"}
(app {:request-method :put, :uri "/ping"})
; nil
Define some middleware and a new 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]}
Nested middleware works too:
(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}
Ring-router supports also 3-arity Async Ring, so it can be used on Node.js too.
Meta-data based extensions
The routing Match is injected into a request and can be extracted with reitit.ring/get-match helper. 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"}
Validating route-tree
TODO
Merging route-trees
TODO
Schema, Spec, Swagger & Openapi
TODO
Interceptors
TODO
Configuring Routers
Routers can be configured via options to do things like custom coercion and compilatin of meta-data. These can be used to do things like clojure.spec validation of meta-data and fast, compiled Ring or Pedestal -style handlers.
The following options are available for the 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 => meta to expand route arg to route meta-data (default reitit.core/expand) |
:coerce |
Function of [path meta] opts => [path meta] to coerce resolved route, can throw or return nil |
:compile |
Function of [path meta] opts => handler to compile a route handler |
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.