A fast data-driven routing library for Clojure/Script
Find a file
Tommi Reiman 4e22fd2f53 Allow middleware to be compiled (fixes #26)
Match :handler => :result
2017-08-30 08:14:06 +03:00
perf-test/clj/reitit Transcude names, fix ataraxy routes 2017-08-22 13:10:39 +03:00
scripts Initial commit 2017-08-07 14:15:45 +03:00
src/reitit Allow middleware to be compiled (fixes #26) 2017-08-30 08:14:06 +03:00
test Allow middleware to be compiled (fixes #26) 2017-08-30 08:14:06 +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 Cleanup 2017-08-20 21:04:35 +03:00
README.md Allow middleware to be compiled (fixes #26) 2017-08-30 08:14:06 +03:00

reitit Build Status Dependencies Status

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
  • Extendable
  • Fast

Ships with example router for Ring. See Issues for roadmap.

Latest version

Clojars Project

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. The actual Router implementation is selected based on the route tree or can be selected with the :router option. Router 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]]]))

:mixed-router is created (both static & wild routes are used):

(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:

(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"
;        :result 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 :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]}

Nested middleware works too:

(app {:request-method :delete, :uri "/api/admin/db"})
; {:status 200, :body [:api :admin :db :delete :handler]}

Async Ring

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. 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"}

Merging route-trees

TODO

Validating meta-data

TODO

Schema, Spec, 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.