A fast data-driven routing library for Clojure/Script
Find a file
2017-08-15 11:09:33 +03:00
perf-test/clj/reitit Cleanup & fix perf test 2017-08-12 17:55:58 +03:00
scripts Initial commit 2017-08-07 14:15:45 +03:00
src/reitit Match is injected into request 2017-08-15 10:05:26 +03:00
test Update README 2017-08-15 11:09:33 +03:00
.gitignore Initial commit 2017-08-07 14:15:45 +03:00
.travis.yml Initial commit 2017-08-07 14:15:45 +03:00
CHANGELOG.md Initial commit 2017-08-07 14:15:45 +03:00
CONTRIBUTING.md Initial commit 2017-08-07 14:15:45 +03:00
LICENSE Initial commit 2017-08-07 14:15:45 +03:00
project.clj Add stuff 2017-08-08 15:31:00 +03:00
README.md Update README 2017-08-15 11:09:33 +03:00

reitit Build Status Dependencies Status

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

Clojars Project

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.