A fast data-driven routing library for Clojure/Script
Find a file
2017-08-20 08:09:31 +03:00
perf-test/clj/reitit compojure-api routing perf 2017-08-20 08:09:31 +03:00
scripts Initial commit 2017-08-07 14:15:45 +03:00
src/reitit Polish docs, more tests 2017-08-19 16:04:44 +03:00
test Initial tests for the opensensors routing 2017-08-19 21:27:25 +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 Sketch for real world perf tests 2017-08-19 16:05:42 +03:00
README.md Polish docs, more tests 2017-08-19 16:04:44 +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
  • 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 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 optional options map as arguments. The route-tree gets expanded, optionally coerced and compiled. 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]]]))

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}]]

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"
;        :handler nil
;        :params {:id "1"}}

Name-based (reverse) routing:

(reitit/match-by-name router ::user)
; #PartialMatch{:template "/api/user/:id",
;               :meta {:name :user/user},
;               :handler nil,
;               :params nil,
;               :required #{:id}}

(reitit/partial-match? (reitit/match-by-name router ::user))
; true

Only a partial match. Let's provide path-parameters:

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

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"
;        :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])))

Backed by a LookupRouter (as no wildcards found):

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

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. 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 => 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.