7 KiB
Route Syntax
Raw routes are defined as vectors, which have a String path, optional (non-sequential) route argument and optional child routes. Routes can be wrapped in vectors and lists and nil routes are ignored. Paths can have path-parameters (:id) or catch-all-parameters (*path).
Simple route:
["/ping"]
Two routes:
[["/ping"]
["/pong"]]
Routes with route arguments:
[["/ping" ::ping]
["/pong" {:name ::pong}]]
Routes with path parameters:
[["/users/:user-id"]
["/api/:version/ping"]]
Route with catch-all parameter:
["/public/*path"]
Nested routes:
["/api"
["/admin" {:middleware [::admin]}
["" ::admin]
["/db" ::db]]
["/ping" ::ping]]
Same routes flattened:
[["/api/admin" {:middleware [::admin], :name ::admin}]
["/api/admin/db" {:middleware [::admin], :name ::db}]
["/api/ping" {:name ::ping}]]
As routes are just data, it's easy to create them programamtically:
(defn cqrs-routes [actions dev-mode?]
["/api" {:interceptors [::api ::db]}
(for [[type interceptor] actions
:let [path (str "/" (name interceptor))
method (condp = type
:query :get
:command :post)]]
[path {method {:interceptors [interceptor]}}])
(if dev-mode? ["/dev-tools" ::dev-tools])])
(cqrs-routes
[[:query 'get-user]
[:command 'add-user]
[:command 'add-order]]
false)
; ["/api" {:interceptors [::api ::db]}
; (["/get-user" {:get {:interceptors [get-user]}}]
; ["/add-user" {:post {:interceptors [add-user]}}]
; ["/add-order" {:post {:interceptors [add-order]}}])
; nil]
Router
Routes are just data and to do actual routing, we need a Router satisfying the reitit.core/Router protocol. Routers are created with reitit.core/router function, taking the raw routes and optionally an options map. Raw routes gets expanded and optionally coerced and compiled.
Router protocol:
(defprotocol Router
(router-name [this])
(routes [this])
(options [this])
(route-names [this])
(match-by-path [this path])
(match-by-name [this name] [this name params]))
Creating a router:
(require '[reitit.core :as r])
(def router
(r/router
[["/api"
["/ping" ::ping]
["/user/:id" ::user]]]))
Router flattens the raw routes and expands the route arguments using reitit.core/Expand protocol. By default, Keywords are expanded to :name and functions are expaned to :handler. nil routes are removed. The expanded routes can be retrieved with router:
(r/routes router)
; [["/api/ping" {:name :user/ping}]
; ["/api/user/:id" {:name :user/user}]]
Path-based routing
Path-based routing is done using the reitit.core/match-by-path function. It takes the router and path as arguments and returns one of the following:
nil, no matchPartialMatch, path matched, missing path-parameters (only in reverse-routing)Match, exact match
(r/match-by-path router "/hello")
; nil
(r/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 routing
All routes which :name route data defined, can be matched by name.
Listing all route names:
(r/route-names router)
; [:user/ping :user/user]
Matching by name:
(r/match-by-name router ::user)
; #PartialMatch{:template "/api/user/:id",
; :meta {:name :user/user},
; :result nil,
; :params nil,
; :required #{:id}}
(r/partial-match? (r/match-by-name router ::user))
; true
We only got a partial match as we didn't provide the needed path-parameters. Let's provide the them too:
(r/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:
(r/match-by-name! router ::user)
; ExceptionInfo missing path-params for route /api/user/:id: #{:id}
Route data
Routes can have arbitrary meta-data, interpreted by the router (via it's :compile hook) or the application itself. For nested routes, route data is accumulated recursively using meta-merge. By default, it appends collections, but it can be overridden to do :prepend, :replace or :displace.
An example router with nested data:
(def router
(r/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:
(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}]]
Route data is returned with Match and the application can act based on it.
(r/match-by-path router "/api/admin/db/users/drop")
; #Match{:template "/api/admin/db/:db/drop"
; :meta {:interceptors [::api ::db]
; :roles #{:db-admin}
; :parameters {:db String}
; :name ::drop-db}
; :result nil
; :params {:db "users"}
; :path "/api/admin/db/users/drop"}
Different Routers
Reitit ships with several different implementations for the Router protocol, originally based on the awesome Pedestal implementation. router selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using :router ROUTER OPTION.
| router | description |
|---|---|
:linear-router |
Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. |
:lookup-router |
Fastest router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters. |
:mixed-router |
Creates internally a :linear-router and a :lookup-router and used them to effectively get best-of-both-worlds. Valid if there are no CONFLICTING ROUTES. |
:prefix-tree-router |
TODO |
The router name can be asked from the router
(r/router-name router)
; :mixed-router