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 (http or https)

  • 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 sets of endpoints 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 on one port, and an inward-facing set of endpoints for operations (status, metrics, management, and so forth) on a different port.

Defining routes and setting up the servlet container are two separate functions; just because you have a route mapped to a port or host doesn’t mean that the servlet container is configured to accept requests for that port or host. You have to connect those dots by setting up the servlet container’s configuration in the service map.

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. Routing Functions 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 [1] 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 fragment. One or more fragments are then combined to form the routing table.

Pedestal will determine which function to invoke based on the kind of data: set, vector, or map:

Diagram

Route specifications are the data values that spell out the possible routes. These are normalized and expanded and combined into a routing table.

A Router Constructor function is passed 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 Routing function encapsulating the routing table and the strategy.

The routing function is passed the request and returns a vector of two values on success: the matching route, then the map of path parameters. On failure, the routing function returns nil.

The routing interceptor combines the routing function and the current request to identify the route, if any, that matched the request. When routing is succesful, the interceptor updates the request map, and queues up route-specific interceptors to handle the request.

There are actually two different implementations of the interceptor; the production one is used in the normal case, where the routing table is an actual value. During development, the routing table may be a function that returns the routing table; in which case, the routing function is rebuilt on every 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(s) 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 it is expressed as 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.

ExpandableRoutes is only needed when Pedestal is provided with routes as data (a Set, Vector, or Map) It is also perfectly valid to build the routes explicitly, by directly calling table-routes.

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.

expand-routes can be passed any number of routing specifications; these can be a mix of different data formats, plus the results of directly calling a definition function, such as table-routes. The routing table is the merged result of all of these.

Routing Table

The expanded routing table is a wrapper object containing a :routes key; The routes is a seq of routing maps, with the following structure:

(:routes
    (route/expand-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 which are used when constructing the full :path-re property.
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 :io.pedestal.http.route.specs/routing-table specification exhaustively defines what is allowed in a routing table.

A path parameter will normally match a single name within the path, delimited by / characters; an alternate form is the wildcard, which starts with * and is only allowed 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.

A path parameter must match at least one term, so a URL of just /accounts or /accounts/ would not match the route.

Builtin Routers

Pedestal includes several routing algorithms; 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

Prefix Tree

High performance, space efficient

:sawtooth

Sawtooth

High performance, reports conflicts, default router

:linear-search

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: a seq of route maps (as described above). The constructor function must return a router function.

Routing Interceptor

The function router is where it all comes together; this function is passed the routing table and the router type; from that it creates the router function, and then, at the end, creates 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-params]

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.


1. But still old - the table format dates back to 2016