Routing Quick Reference
Routing is the process by which an incoming request is analyzed to determine how it will be processed by Pedestal.
Routing is based on comparing any of a number of factors from the request, including:
-
The request path (including matching wild cards in the path)
-
The HTTP method used (GET, POST, etc.)
-
The HTTP scheme used in the request
-
The name of host in the request
-
The port number
It’s natural to understand why path and method are involved in routing; less obvious for the other factors. Essentially, a single Pedestal application may host two or more concurrent applications inside the same server - incoming requests are differentiated most often by port, but can also be differentiated by the host. A somewhat common example is an outward-facing consumer application exposed outside a company’s firewall, and an inward-facing admin application that is only available inside the firewall.
Library
The library io.pedestal.http.route
namespace
provides the core components to express routes and construct
routers. It can be used independently of the pedestal-service library.
Route Specifications vs. Routers vs. Routing Interceptor
Pedestal is data driven, and the routing information starts in one form (as data) and is then transformed in a couple of stages to reach a final result that can be used to route incoming requests.
The start is the route specification. Pedestal has three built-in formats for route specification: table, terse, and verbose. Table is the newest format (but still old - it dates back to 2016) and is the preferred format; the others are maintained for compatibility.
For each of the built-in formats, there is a function that can convert that specific format into a routing table (also known as expanded routes). Pedestal can often figure out which function to invoke based on the kind of data: set, vector, or map:
Route specifications are the data values that spell out the possible routes. These are normalized and expanded into a routing table.
A Router Constructor function is provided with the routing table, and applies a specific strategy to match incoming requests to the provided routes; there are a few different constructor functions, each with different limitations and trade-offs.
The constructor returns a
Router
instance encapsulating the routing table and the
strategy.
The routing interceptor builds on a Router
to dispatch incoming requests to routes
defined by the route specification. It identifies the route matching the incoming request
and queues up route-specific interceptors to handle the request.
Generally, all of this is automatic; an application provides a route specification in the :io.pedestal.http/route key of the service map (and perhaps a value for :io.pedestal.http/router) and a routing interceptor is automatically created.
Route Specifications
Route specifications go through a series of transformations that results in a routing table; a routing table is a sequence of verbose routing maps.
The
expand-routes
function converts any kind of specification into a routing table:
Argument to expand-routes |
Syntax used |
---|---|
Set |
Table Syntax - Most recent (2016), easier than terse format, recommended for new projects. |
Vector |
Terse Syntax - improvement on the verbose syntax, with an emphasis on avoiding redundancy, but can be hard to read and write, since its deeply nested maps. |
Map |
Verbose Syntax - oldest format. |
This mapping from value type to specification type is the responsibility of the
ExpandableRoutes
protocol, which is
extended on Map, Set, and Vector.
It’s not impossible that your application has specific needs that can’t be met by any of the built-in formats and, as is often the case in Pedestal. To support a new syntax, come up with a syntax that resolves to new record type that implements the ExpandableRoutes protocol.
Routing Table
The expanded routing table is a list of maps, each with the following specific structure:
(table-routes #{{:app-name :example-app
:scheme :https
:host "example.com"}
["/department/:id/employees" :get [...]
:route-name :org.example.app/employee-search
:constraints {:name #".+"
:order #"(asc|desc)"}]})
=>
({:route-name :org.example.app/employee-search (1)
:app-name :example-app (2)
:path "/department/:id/employees" (3)
:method :get (4)
:scheme :https (5)
:host "example.com" (6)
:port 8080 (7)
:interceptors [...] (8)
:path-re #"/\Qdepartment\E/([^/]+)/\Qemployees\E" (9)
:path-parts ["department" :id "employees"] (10)
:path-params [:id] (11)
:path-constraints {:id "([^/]+)"} (12)
:query-constraints {:name #".+" (13)
:order #"(asc|desc)"}
})
1 | :route-name is required and must be a keyword; often a qualified keyword. The route name must be unique within the table. |
2 | Optional, used for documentation only. |
3 | Must start with a leading slash; terms with a leading : identify path parameters, or
a leading * identifies a wildcard. |
4 | HTTP method to match against, or :any to match any HTTP method. |
5 | Optional for matching, must be :http or :https. |
6 | Optional for matching. |
7 | Optional for matching. |
8 | A vector of interceptors (converted via IntoInterceptor ) |
9 | A regular expression, generated from the path, that can match an incoming path and provide capture groups for path parameters. |
10 | The parts of the path, as strings or keywords. |
11 | The path parameters, in the order they appear in the path. |
12 | Path constraints[1] |
13 | Query constraints, which maps keywords corresponding to query parameters to regular expressions used to match the parameters. |
This ficticious example defines a URI that includes an id in the request path, but has also defined query parameters constraints.
The routing-table
specification exhaustively defines what is allowed in a routing table.
A path parameter will normally match a single name with in the path, delimited by /
characters; an alternate form
is the wildcard,, which starts with *
and must come at the end of the path: ["/accounts/*ids :get …]
would
provide a path parameter named :ids, and will contain anything on the URL after /accounts/
, including any slashes.
Builtin Routers
Pedestal includes several Routers; this reflects not only the evolution of the Pedestal library, but also allows for different trade-offs in the algorithm used by each Router. In rare cases, an application can provide its own Router rather than use one of Pedestal’s.
When your application starts a Pedestal service with
create-servlet
or
create-server
,
Pedestal creates a router, using the following keys from the service map:
Key | Meaning |
---|---|
:io.pedestal.http/routes |
A route specification |
:io.pedestal.http/router |
Key to select a router, or a function that constructs a router from a routing table |
When the value of :io.pedestal.http/router is a keyword, it selects one of the built-in algorithms:
Keyword | Router | Performance |
---|---|---|
:map-tree |
Map Tree (default) |
Very fast |
:prefix-tree |
High performance, space efficient |
|
:linear-search |
Lowest performance |
Custom Router
When the value of :io.pedestal.http/router is a function, that
function is used to construct a router. The function must take one
argument: the fully expanded routing table. The constructor function must
return a value that satisfies the
Router
protocol.
So the function is passed the routing table and returns a Router
for those routes.
The Router
is supplied with the incoming request, and returns the matching route map
(a verbose route map extended with extracted parameters from the path).
Routing Interceptor
The function router
is where it all comes together;
this function is passed the route specification and, optionally, the router type; from that it creates
the routing table, and passes that through the correct Router constructor function,
obtaining at the end an interceptor that performs routing, which it returns.
During request execution, on a successful routing, the following keys are added in the context map:
Key / Key Path | Value |
---|---|
:route |
The verbose route map |
[:request :path-parameters] |
Path parameters extracted from the request path |
In addition, additional interceptors, specific to the route, will have been
scheduled for execution via the enqueue
function.
On failure, when the router does not match any route, the context key :route is set to nil.