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:

Diagram

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.

ExpandableRoutes is only needed when Pedestal is provided with routes but doesn’t know what format they are in. It is also perfectly valid to build the routes explicitly, by directly calling table-routes, and then building your own routing interceptor.

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 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 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

Prefix Tree

High performance, space efficient

:sawtooth

Sawtooth

High performance, reports conflicts

: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: 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.