mirror of
https://github.com/metosin/reitit.git
synced 2025-12-27 20:08:25 +00:00
653 lines
21 KiB
Markdown
653 lines
21 KiB
Markdown
# reitit [](https://travis-ci.org/metosin/reitit) [](https://jarkeeper.com/metosin/reitit)
|
|
|
|
A friendly data-driven router for Clojure(Script).
|
|
|
|
* Simple data-driven [route syntax](#route-syntax)
|
|
* First-class [route meta-data](#route-meta-data)
|
|
* Generic, not tied to HTTP
|
|
* [Route conflict resolution](#route-conflicts)
|
|
* [Pluggable coercion](#parameter-coercion) ([clojure.spec](https://clojure.org/about/spec))
|
|
* both [Middleware](#middleware) & Interceptors
|
|
* Extendable
|
|
* Fast
|
|
|
|
Ships with example router for [Ring](#ring). See [Issues](https://github.com/metosin/reitit/issues) for roadmap.
|
|
|
|
## Latest version
|
|
|
|
[](http://clojars.org/metosin/reitit)
|
|
|
|
## 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:
|
|
|
|
```clj
|
|
["/ping"]
|
|
```
|
|
|
|
Two routes:
|
|
|
|
```clj
|
|
[["/ping"]
|
|
["/pong"]]
|
|
```
|
|
|
|
Routes with meta-data:
|
|
|
|
```clj
|
|
[["/ping" ::ping]
|
|
["/pong" {:name ::pong}]]
|
|
```
|
|
|
|
Routes with path and catch-all parameters:
|
|
|
|
```clj
|
|
[["/users/:user-id"]
|
|
["/public/*path"]]
|
|
```
|
|
|
|
Nested routes with meta-data:
|
|
|
|
```clj
|
|
["/api"
|
|
["/admin" {:middleware [::admin]}
|
|
["/user" ::user]
|
|
["/db" ::db]
|
|
["/ping" ::ping]]
|
|
```
|
|
|
|
Same routes flattened:
|
|
|
|
```clj
|
|
[["/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](https://github.com/pedestal/pedestal/tree/master/route) 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:
|
|
|
|
```clj
|
|
(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):
|
|
|
|
```clj
|
|
(reitit/router-name router)
|
|
; :mixed-router
|
|
```
|
|
|
|
The expanded routes:
|
|
|
|
```clj
|
|
(reitit/routes router)
|
|
; [["/api/ping" {:name :user/ping}]
|
|
; ["/api/user/:id" {:name :user/user}]]
|
|
```
|
|
|
|
Route names:
|
|
|
|
```clj
|
|
(reitit/route-names router)
|
|
; [:user/ping :user/user]
|
|
```
|
|
|
|
### Path-based routing
|
|
|
|
```clj
|
|
(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
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(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](https://github.com/weavejester/meta-merge).
|
|
|
|
A router based on nested route tree:
|
|
|
|
```clj
|
|
(def router
|
|
(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:
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(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](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:result` in the match. See [configuring routers](#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.
|
|
|
|
```clj
|
|
(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](https://github.com/ring-clojure/ring)-router adds support for ring concepts like [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It runs a custom route compiler, creating a optimized stucture for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined.
|
|
|
|
Simple Ring app:
|
|
|
|
```clj
|
|
(require '[reitit.ring :as ring])
|
|
|
|
(defn handler [_]
|
|
{:status 200, :body "ok"})
|
|
|
|
(def app
|
|
(ring/ring-handler
|
|
(ring/router
|
|
["/ping" handler])))
|
|
```
|
|
|
|
Applying the handler:
|
|
|
|
```clj
|
|
(app {:request-method :get, :uri "/favicon.ico"})
|
|
; nil
|
|
|
|
(app {:request-method :get, :uri "/ping"})
|
|
; {:status 200, :body "ok"}
|
|
```
|
|
|
|
The expanded routes:
|
|
|
|
```clj
|
|
(-> app (ring/get-router) (reitit/routes))
|
|
; [["/ping"
|
|
; {:handler #object[...]}
|
|
; #Methods{:any #Endpoint{:meta {:handler #object[...]},
|
|
; :handler #object[...],
|
|
; :middleware []}}]]
|
|
```
|
|
|
|
Note that the compiled resuts as third element in the route vector.
|
|
|
|
### Request-method based routing
|
|
|
|
Handler are also looked under request-method keys: `:get`, `:head`, `:patch`, `:delete`, `:options`, `:post` or `:put`. Top-level handler is used if request-method based handler is not found.
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(-> app
|
|
(ring/get-router)
|
|
(reitit/match-by-name ::ping)
|
|
:path)
|
|
; "/ping"
|
|
```
|
|
|
|
### Middleware
|
|
|
|
Middleware can be added with a `:middleware` key, with a vector value of the following:
|
|
|
|
1. ring middleware function `handler -> request -> response`
|
|
2. vector of middleware function `handler ?args -> request -> response` and optinally it's args.
|
|
|
|
A middleware and a handler:
|
|
|
|
```clj
|
|
(defn wrap [handler id]
|
|
(fn [request]
|
|
(handler (update request ::acc (fnil conj []) id))))
|
|
|
|
(defn handler [{:keys [::acc]}]
|
|
{:status 200, :body (conj acc :handler)})
|
|
```
|
|
|
|
App with nested middleware:
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(app {:request-method :delete, :uri "/api/ping"})
|
|
; {:status 200, :body [:api :handler]}
|
|
```
|
|
|
|
```clj
|
|
(app {:request-method :delete, :uri "/api/admin/db"})
|
|
; {:status 200, :body [:api :admin :db :delete :handler]}
|
|
```
|
|
|
|
### Middleware Records
|
|
|
|
Reitit supports first-class data-driven middleware via `reitit.middleware/Middleware` records, created with `reitit.middleware/create` function. The following keys have special purpose:
|
|
|
|
| key | description |
|
|
| -----------|-------------|
|
|
| `:name` | Name of the middleware as qualified keyword (optional,recommended for libs)
|
|
| `:wrap` | The actual middleware function of `handler args? => request => response`
|
|
| `:gen` | Middleware compile function, see [compiling middleware](#compiling-middleware).
|
|
|
|
When routes are compiled, all middleware are expanded (and optionally compiled) into `Middleware` and stored in compilation results for later use (api-docs etc). For actual request processing, they are unwrapped into normal middleware functions producing zero runtime performance penalty. Middleware expansion is backed by `reitit.middleware/IntoMiddleware` protocol, enabling plain clojure(script) maps to be used.
|
|
|
|
A Record:
|
|
|
|
```clj
|
|
(require '[reitit.middleware :as middleware])
|
|
|
|
(def wrap2
|
|
(middleware/create
|
|
{:name ::wrap2
|
|
:description "a nice little mw, takes 1 arg."
|
|
:wrap wrap}))
|
|
```
|
|
|
|
As plain map:
|
|
|
|
```clj
|
|
;; plain map
|
|
(def wrap3
|
|
{:name ::wrap3
|
|
:description "a nice little mw, :api as arg"
|
|
:wrap (fn [handler]
|
|
(wrap handler :api))})
|
|
```
|
|
|
|
### Async Ring
|
|
|
|
All built-in middleware provide both 2 and 3-arity and are compiled for both Clojure & ClojureScript, so they work with [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) and [Node.js](https://nodejs.org) too.
|
|
|
|
### Meta-data based extensions
|
|
|
|
`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build dynamic extensions to the system.
|
|
|
|
Example middleware to guard routes based on user roles:
|
|
|
|
```clj
|
|
(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):
|
|
|
|
```clj
|
|
(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:
|
|
|
|
```clj
|
|
(app {:request-method :get, :uri "/api/ping"})
|
|
; {:status 200, :body "ok"}
|
|
```
|
|
|
|
Anonymous access to guarded route:
|
|
|
|
```clj
|
|
(app {:request-method :get, :uri "/api/admin/ping"})
|
|
; {:status 403, :body "forbidden"}
|
|
```
|
|
|
|
Authorized access to guarded route:
|
|
|
|
```clj
|
|
(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}})
|
|
; {:status 200, :body "ok"}
|
|
```
|
|
|
|
## Parameter coercion
|
|
|
|
Reitit provides pluggable parameter coercion via `reitit.coercion.protocol/Coercion` protocol, originally introduced in [compojure-api](https://clojars.org/metosin/compojure-api). Reitit ships with `reitit.coercion.spec/SpecCoercion` providing implemenation for [clojure.spec](https://clojure.org/about/spec) and [data-specs](https://github.com/metosin/spec-tools#data-specs).
|
|
|
|
**NOTE**: Before Clojure 1.9.0 is shipped, to use the spec-coercion, one needs to add the following dependencies manually to the project:
|
|
|
|
```clj
|
|
[org.clojure/clojure "1.9.0-alpha20"]
|
|
[org.clojure/spec.alpha "0.1.123"]
|
|
[metosin/spec-tools "0.3.3"]
|
|
```
|
|
|
|
### Ring request and response coercion
|
|
|
|
To use `Coercion` with Ring, one needs to do the following:
|
|
|
|
1. Define parameters and responses as data into route meta-data, in format adopted from [ring-swagger](https://github.com/metosin/ring-swagger#more-complete-example):
|
|
* `:parameters` map, with submaps for different parameters: `:query`, `:body`, `:form`, `:header` and `:path`. Parameters are defined in the format understood by the `Coercion`.
|
|
* `:responses` map, with response status codes as keys (or `:default` for "everything else") with maps with `:schema` and optionally `:description` as values.
|
|
2. Define a `Coercion` to route meta-data under `:coercion`
|
|
3. 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
|
|
|
|
```clj
|
|
(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}})))
|
|
```
|
|
|
|
|
|
```clj
|
|
(app
|
|
{:request-method :get
|
|
:uri "/api/ping"
|
|
:body-params {:x 1, :y 2}})
|
|
; {:status 200, :body {:total 3}}
|
|
```
|
|
|
|
#### Example with specs
|
|
|
|
```clj
|
|
(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}})))
|
|
```
|
|
|
|
```clj
|
|
(app
|
|
{:request-method :get
|
|
:uri "/api/ping"
|
|
:body-params {:x 1, :y 2}})
|
|
; {:status 200, :body {:total 3}}
|
|
```
|
|
|
|
## Compiling Middleware
|
|
|
|
The [meta-data extensions](#meta-data-based-extensions) are a easy way to extend the system. Routes meta-data can be transformed into any shape (records, functions etc.) in route compilation, enabling fast access at request-time.
|
|
|
|
Still, we can do better. As we know the exact route that 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 - yielding faster runtime processing.
|
|
|
|
To do this we use [middleware records](#middleware-records) `:gen` hook instead of the normal `:wrap`. `:gen` expects a function of `route-meta router-opts => wrap`. Middleware can also return `nil`, which effective unmounts the middleware. Why mount a `wrap-enforce-roles` middleware for a route if there are no roles required for it?
|
|
|
|
To demonstrate the two approaches, below are response coercion middleware written as normal ring middleware function and as middleware record with `:gen`. These are the actual codes are from [`reitit.coercion`](https://github.com/metosin/reitit/blob/master/src/reitit/coercion.cljc):
|
|
|
|
### Naive
|
|
|
|
* Extracts the compiled route information on every request.
|
|
|
|
```clj
|
|
(defn wrap-coerce-response
|
|
"Pluggable response coercion middleware.
|
|
Expects a :coercion of type `reitit.coercion.protocol/Coercion`
|
|
and :responses 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 `:coercion` and `:responses` are defined for the route
|
|
|
|
```clj
|
|
(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/create
|
|
{:name ::coerce-response
|
|
: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)))))))}))
|
|
```
|
|
|
|
The `:gen` -version has 50% less code, is easier to reason about and is 2-4x faster on basic perf tests.
|
|
|
|
## 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`](https://clojure.org/about/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
|
|
| `: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](https://github.com/weavejester/ataraxy), [Bide](https://github.com/funcool/bide), [Bidi](https://github.com/juxt/bidi), [Compojure](https://github.com/weavejester/compojure) and
|
|
[Pedestal](https://github.com/pedestal/pedestal/tree/master/route).
|
|
|
|
Also to [Compojure-api](https://github.com/metosin/compojure-api), [Kekkonen](https://github.com/metosin/kekkonen) and [Ring-swagger](https://github.com/metosin/ring-swagger) and for the data-driven syntax, coercion & stuff.
|
|
|
|
And some [Yada](https://github.com/juxt/yada) too.
|
|
|
|
## License
|
|
|
|
Copyright © 2017 [Metosin Oy](http://www.metosin.fi)
|
|
|
|
Distributed under the Eclipse Public License, the same as Clojure.
|