Pedestal's HTTP service plumbing provides a mechanism for routing requests through an ordered list of interceptors that handle them. The same infrastructure supports generating URLs that, when used with the appropriate HTTP verb, cause a request to be routed to a particular interceptor list. This document describes how routing and URL generation work.
Pedestal's HTTP routing and URL generation features are driven by a route table. A route table is a sequence of routes. A route is a map containing criteria for matching an HTTP request and an ordered list of interceptors to invoke on a request that matches a particular route.
A route matching is based on:
- URL scheme
- HTTP method
- Host header
- URL path
- Constraints on param values in URL path and/or query string
A route table is simply a data structure; in our case, it is a sequence of maps. The structure caters to the needs of matching and dispatching of requests, and as such has a great deal of repeated and derived data intended for use in that process. Creating the data structure in its final, verbose form by hand would be very tedious.
We've built a simpler, terse form for route tables. The terse form is also a data structure, albeit more explicitly hierarchical; Writing a route table in the terse form is easier because information is not explicitly duplicated. Instead, child nodes implicitly inherit relevant route data from their ancestors.
It is important to note that the terse form is a convenient way to define route tables, nothing more. It is always expanded to the more verbose structure - a sequence of maps - before use. While a convenient authoring format, it is not directly used to route requests or generate URLs.
In the terse format, a route table is a vector of vectors, each describing an application. Each application vector can contain the following optional elements:
- a keyword identifying the application by name (optional)
- a URL scheme (optional)
- a host name (optional)
- one or more nested vectors specifying routes (required)
Here is a simple "Hello World" example:
[[:hello-world :http "example.com"
["/hello-world" {:get hello-world}]]]
In this case, the following HTTP request:
GET /hello-world HTTP/1.1
Host: example.com
would be routed to the hello-world
interceptor.
A request to a different host (either DNS name or IP address) or using HTTPS would not be routed, unless the application's specification were loosened, like so:
[[:hello-world
["/hello-world" {:get hello-world}]]]
The application's name, :hello-world
, is optionally used during URL
generation, and can also be omitted, leaving this:
[[["/hello-world" {:get hello-world}]]]
This is the smallest possible example of a useful route table.
In most cases, a nested vector specifying routes contains a path and a verb
map (there are exceptions, explained below). The verb map contains
keys corresponding to HTTP verbs. All verbs are supported, along with
the special value :any
, indicating a match to any HTTP verb. Each
verb represents a different route. The values in the verb map
represent the "destination interceptor". Additional intermediate
interceptors may also be invoked, as described below.
The value for a key in a route's verb map specifies a route's destination interceptor. The value can be:
-
a symbol that resolves to one of:
-
a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler)
-
an interceptor
-
a function that returns an interceptor and is marked with metadata ^{:interceptorfn true}
-
-
a vector containing the following:
-
an optional keyword that names the route, for use in URL generation
-
a value that is either:
-
a symbol interpreted as described above
-
a list that evaluates to either:
-
a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler)
-
an interceptor
-
-
-
an optional vector of interceptors, described below
-
an optional map of constraints, described below
-
The following sections explains how these values are used.
A terse route definition must be expanded to a full route table before it can be used. There are two ways to do this:
-
the
io.pedestal.http.route/expand-routes
function -
the
io.pedestal.http.route.definition/defroutes
macro
The expand-routes
function takes a terse route definition data
structure as input and returns a route table. For example:
(defn hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes '[[["/hello-world" {:get hello-world}]]]))
Note that the terse data structure is quoted, making hello-world
a
symbol. It resolves the hello-world
function, which takes a Ring
request and returns a Ring response.
The defroutes
macro is equivalent to calling expand-routes
with a
quoted data structure:
(defroutes route-table
[[["/hello-world" {:get hello-world}]]])
A quoted terse route definition is read at load time and is static after that. In some cases, you may need to dynamically generate routes. Here is an example:
(defn hello-fn [who]
(fn [req] (ring.util.response/response (str "Hello " who)))
(defn make-routes-for-who [who]
(expand-routes
`[[["/hello" {:get [:hello-who (hello-fn ~who)]}]]]))
(def route-table (make-routes-for-who "World"))
In this case, the make-routes-for-who
function takes an argument,
who
, that it uses to configure the resulting routes. It generates
the terse route data structure using Clojure's syntax quote mechanism,
splicing in the value of who
where it is needed.
In some cases, you may want to assemble the terse data structure without quoting it at all. Here is an example:
(defn hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes
[[["/hello-world"
{:get [(handler ::hello-world hello-world)]}]]]))
In this case, the hello-world
symbol is resolved to the
hello-world
function as the data structure is built. The handler
function (defined in io.pedestal.interceptor
) takes the
function and builds an interceptor from it, to meet the requirement
that a value in a verb map must be a symbol, an interceptor, or a list.
Alternatively, hello-world
can be defined as an interceptor
directly, using the io.pedestal.interceptor.helpers/defhandler
macro:
(defhandler hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes
[[["/hello-world" {:get hello-world}]]]))
Or, hello-world
can be quoted, making it a symbol again:
(defn hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes
[[["/hello-world" {:get 'hello-world]}]]]))
The expand-routes
function is more flexible, but also harder to use
than the defroutes
macro. The latter is preferred in most cases.
This section describes path parameters, hierarchical route definitions, intermediate interceptors and constraints.
Segments of a route's path may be parameterized simply by prepending ':' to the segment's name:
(defn hello-who [req]
(let [who (get-in req [:path-params :who])]
(ring.util.response/response (str "Hello " who))))
(defroutes route-table [[["/hello/:who" {:get hello-who}]]])
As with Ring, Rails, etc, the path parameters are parsed and added to the request's param map.
Splat parameters are also supported. They are defined using a final path segment prepended with '*', like this:
[[["/hello/:who" {:get hello-who}]
["/*other" {:get get-other-stuff}]]]
Route definitions in the terse form are hierarchical. A route definition may contain zero or more child routes. A child route inherits information from it's ancestors.
Here is an example showing how a path is inherited:
[[["/order" {:get list-orders
:post create-order}
["/:id" {:get view-order
:put update-order}]]]]
This defines these four routes:
-
GET /order
-
POST /order
-
GET /order/:id
-
PUT /order/:id
The "/order" path segment is inherited by the child routes.
It is worth noting that this same structure could be defined without hierarchy:
[[["/order" {:get list-orders
:post create-order}]
["/order/:id" {:get view-order
:put update-order}]]]
This would produce the same four routes.
Every route definition includes an interceptor path that will be executed for any request that matches the route. By default, a route's interceptor path contains one interceptor, the route's handler. For instance, the four routes defined in the previous section have the following interceptor paths:
-
GET /order => [list-orders]
-
POST /order => [create-order]
-
GET /order/:id => [view-order]
-
PUT /order/:id => [update-order]
Route definitions can specify additional interceptors to include in
the interceptor path for a given route. These interceptors function as
before, after or around filters for specific routes. They are
specified using a vector marked with ^:interceptors
metadata. The
values specified in the interceptors vector may be:
-
a symbol that resolves to one of:
-
an interceptor
-
a function that returns an interceptor and is marked with metadata ^{:interceptorfn true}
-
a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler)
-
-
a list that evaluates to either:
-
an interceptor
-
a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler)
-
Here is an example:
[[["/order" {:get list-orders
:post create-order}
["/:id" ^:interceptors [load-order-from-db]
{:get view-order
:put update-order}]]]]
With this additional interceptor specified for the second two routes, the interceptor paths become:
-
GET /order => [list-orders]
-
POST /order => [create-order]
-
GET /order/:id => [load-order-from-db view-order]
-
PUT /order/:id => [load-order-from-db update-order]
Any number of interceptors may be specified as an order sequence:
[[["/order" {:get list-orders
:post create-order}
["/:id" ^:interceptors [load-order-from-db
verify-order-ownership]
{:get view-order
:put update-order}]]]]
In this case, for requests that match the "/order/:id" route, the
load-order-from-db
interceptor will run before the
verify-order-ownership
interceptor, then the appropriate handler,
view-order
or update-order
, will run.
Interceptors may be specified at multiple levels of the hierarchy. Like paths, interceptors are inherited. Inherited interceptors always come first in the interceptor path for a given route.
[[["/order" ^:interceptors [verify-request]
{:get list-orders
:post create-order}
["/:id" ^:interceptors [verify-order-ownership
load-order-from-db]
{:get view-order
:put update-order}]]]]
This definition produces the following routes and interceptor paths:
-
GET /order => [verify-request list-orders]
-
POST /order => [verify-request create-order]
-
GET /order/:id => [verify-request verify-order-ownership load-order-from-db view-order]
-
PUT /order/:id => [verify-request verify-order-ownership load-order-from-db update-order]
Inherited interceptors always precede a route definition's own handlers in its interceptor path.
A route may specify constraints on path parameters and query string parameters. Constraints are tested when a request is being matched against a route. If the request does not satisfy a route's constraints, it is not considered a match.
Constraints are specified as a map marked with ^:constraints
metadata. The keys in the map are path parameters or query string
parameters. The values are regular expressions used for testing
parameter values.
Here is an example of how constraints can be used:
["/user" {:get list-users
:post add-user}
["/:user-id"
^:constraints {:user-id #"[0-9]+"}
{:put update-user}
[^:constraints {:view #"long|short"}
{:get view-user}]]]
This defines four routes:
-
GET /user => [list-users]
-
POST /user => [add-user]
-
PUT /user/:user-id => [update-user], but only if :user-id matches [0-9]+
-
GET /user/:user-id => [view-user], but only if :user-id matches [0-9]+ and there is a "view" query param whose value is either "long" or "short"
Note that constraints can be used in addition to or in place of a path when defining a child route. In that case, they must appear as the first item in the child route vector.
Like intermediate interceptors, constraints are inherited by child routes.
Once a route table is defined, it can be used to create a router. The
io.pedestal.http.route/router
function takes a route table as
input and returns an interceptor that handles routing.
(defn hello-world [req] {:status 200 :body "Hello World!"})
(defroutes master-routes
[[["/hello-world" {:get hello-world}]]])
(def router (router master-routes))
When a routing interceptor's enter function is invoked, it attempts to match the incoming request against each route in the route table in turn. If a route matches, the routing interceptor adds all the interceptors for the given route to the current interceptor path. They will be invoked by the interceptor engine after the router's function completes. It also adds the selected route to the interceptor context map so that other interceptors can know which route was selected.
If no route matches, the router simply returns the current interceptor context without modification.
During development it is useful to be able to reprocess route
definitions without restarting your server. If you call the router
function and pass a function that returns a route table, it will be
called every time the routing interceptor is used. This allows your
Web server to use the latest compiled routes without restarting.
(def router (router #(deref #'master-routes)))
If you are using the Pedestal service template for lein, it provides a default route table and handles setting up a routing interceptor as one of the steps of building a service. It also configures use of the latest compiled routes when running in the repl.
Every route has a name, represented as a keyword. Route names are implicit, where possible. For routes that specify destination interceptors using symbols, the name is the fully-qualified symbol name expressed as a keyword.
For routes that specify destination interceptors directly as interceptor values, the route-name is the name of the interceptor.
For interceptors defined using the defbefore
, defafter
,
defaround
, defon-request
, defhandler
and defon-response
macros
in the io.pedestal.interceptor
namespace, the name is the
interceptor's fully-qualified symbol name expressed as a keyword.
For interceptors defined using the before
, after
, around
,
on-request
, handler
and on-response
functions in the
io.pedestal.interceptor
namespace, the name is the keyword
passed to the function, if any.
For routes that specify interceptors indirectly as lists to be evaluated, no route name can be implicitly assigned.
You can specify an explicit route name for any route by adding a keyword as the first item in the vector specified as the value of a given HTTP verb for a given route. Explicit route names take precedence over implicit names. For routes that cannot be given an implicit name, an explicit name must be provided or an exception will be thrown during route expansion.
Here is an example.
(require '[orders :as o])
(defroutes routes
[[["/order" {:get o/list-orders
:post [:make-an-order o/create-order]}
^:interceptors [verify-request]
["/:id" {:get o/view-order
:put o/update-order}
^:interceptors [o/verify-order-ownership
o/load-order-from-db]]]]])
In this case, the destination interceptors are all specified as
symbols in the orders
namespace. The route names are listed below:
-
GET /order => :orders/list-orders
-
POST /order => :make-an-order
-
GET /order/:id => :orders/view-order
-
POST /order/:id => :orders/update-order
The second route specified an explicit route name, :make-an-order
,
which takes precedence over the implicit name for that route,
:orders/create-order
.
The io.pedestal.http.route/print-routes
helper function prints
route verbs, paths and names at the repl. When in doubt, you can use
it to find route names.