Defining Routes

Welcome

This guide takes us past the basics of getting started. It deals with a part of Pedestal that you will touch quite often: routing.

A key problem in any backend service is directing incoming requests to the right bit of code. As an industry, we’ve settled on the term "routing" to describe how a service understands a request’s URL and invokes a matching function.

There’s a flip side to routing, though, which is generating URLs to put into links and hrefs. If we’re not careful, link generation can create hard-to-find coupling between different parts of a service.

Pedestal addresses both parts of this problem with the same feature.

What You Will Learn

After reading this guide, you will be able to:

  • Define routes

  • Parse parameters from URLs

  • Connect routes to handlers

  • Apply interceptors along a route

  • Use constraints to ensure a route is invoked correctly

  • Generate links for a route during a request to that route

  • Generate links for other routes

Guide Assumptions

This guide assumes that you understand HTTP requests and URLs. In particular, we make no effort to explain URL-encoding or query strings.

Getting Help If You’re Stuck

We’ll take this in small steps. If you get stuck at any point in this guide, please submit an issue about this guide or hop over to the mailing list and raise your hand there.

Where We Are Going

In Your First API, we defined a handful of routes for a basic REST style API. It’s time to dig deeper and understand what we can do with this flexible and powerful part of Pedestal.

We will work with routes that:

  • Serve static resources

  • Use multiple parameters

  • Use wildcards to match subtrees

  • Support multiple verbs

We will also generate URLs from routes and parameters.

Before We Begin

This guide will use Leiningen to generate and build a template project. If you haven’t already installed it, please take a few minutes to set it up.

A Note About Routing Syntax

Since Pedestal was first unveiled, we’ve gone through a couple of iterations on the routing syntax. So far, these iterations have all been additive. Prior to release 0.5.0, guides and samples all used the "terse" syntax, which is written as a triply-nested vector. The terse syntax is powerful but not easy.

Release 0.5.0 introduced a new syntax we call the "table" syntax. It is more wordy and has some repetition from row to row, but it also has some advantages:

  1. The parser is simpler and produces better error messages when the input is not right. (This includes some work to make stack traces more helpful.)

  2. The table does not have hierarchical nesting, so the rows are independent.

  3. The input is just data, so you can read it from an EDN file or compose it with regular functions. No more syntax-quoting to create interceptors with parameters.

The "terse" and "verbose" syntax are both still supported, until we hear from the community. We’ve also put some effort into better error messages when dealing with the terse format.

See the Routing Quick Reference for full details.

Defining a Route: The Bare Minimum

The simplest route has just a URL, an HTTP verb, and a handler:

["/users" :get view-users]

In this case, view-users is anything that can be resolved as an interceptor:

  • An Interceptor record

  • A function that returns an interceptor (the function must be annotated as ^:interceptor-fn)

  • A request handler function (really a special case of interceptor)

Building handlers

There’s nothing special about using a symbol in the handler’s position. You could call a function that returns an interceptor:

["/users" :get (make-view-users-handler db-conn)]

That call to make-view-users-handler is nothing special. Clojure will evaluate it when you build the route table. Just make sure it returns something that can be resolved as an interceptor.

That also means you can use anonymous functions as handlers. Here’s another way we could implement an "echo" function:

["/echo" :get (fn [request] {:status 200 :body request}) :route-name :echo]

Every route must have a route name, but in most cases, Pedestal can "figure out" a reasonable value. Here,the route name clause is necessary here because an anonymous function has nothing that Pedestal can use to infer a route name.

Path Parameters

The URL in a route doesn’t have to be just text, often important request information is included in the URL, rather than query parameters. The URL from the route is actually pattern used to match URLs.

The URL pattern can include any number of segments to capture as parameters. Any segment that looks like a keyword will be captured in the :path-params map in the request.

So the route:

["/users/:user-id" :get view-users]

will match any of the following requests:

  • /users/abacab

  • /users/miken

  • /users/12345

  • /users/mike%20n

When the request reaches our view-users handler, it will include a map like this:

{:path-params {:user-id "miken"}}

The path parameters are always delivered as strings. The strings are HTTP decoded for you, but are not otherwise converted.

A single path parameter only matches one segment of a URL. One segment is just the part between '/' characters. So the route above will not match /users/miken/profile/photos/blue-wig.jpg.

What if our user IDs are all numeric? It would be convenient if the route would only match when the URL meets a valid pattern in the path parameters. That’s the job of Constraints, discussed below.

Catch-all Parameters

What if you actually do want to match any number of segments after a path? In that case, you use a catch-all parameter. It looks like a path parameter, except it has an asterisk instead of a colon:

["/users/:user-id/profile/*subpage" :get view-user-profile]

Now this route does match the URL /users/miken/profile/photos/blue-wig.jpg and, as you might have guessed, the matching segments are still delivered as path parameters in the request map:

{:path-params {:user-id "miken" :subpage "photos/blue-wig.jpg"}}

As the subpage path parameter demonstrates, catch-all parameters are strings containing the remaining path segments.

Query Parameters

You don’t need to do anything in the route to capture query parameters. They are automatically parsed and passed in the request map, under the :query-params key. Like path parameters, query parameters are always delivered as HTTP-decoded strings.

For example, an HTTP request with for the URL:

/search?q=blog

will have this in the request map:

{:query-params {:q "blog"}}

Verbs

So far, all our examples have used :get as the HTTP verb. Pedestal supports the following verbs:

  • :get

  • :put

  • :post

  • :delete

  • :patch

  • :options

  • :head

  • :any

These should look familiar, except for :any. The :any keyword is a wildcard that allows a route to match any request method. That gives a handler the opportunity to decide whether a request method is allowed or not.

Interceptors

So far, all our examples have used just one handler function. But one of Pedestal’s key features is the ability to create a chain of interceptors. The route table allows you to put a vector of interceptors (or things that resolve to interceptors) in that third position.

["/user/:user-id/private" :post [inject-connection
                                 auth-required
                                 (body-params/body-params)
                                 view-user]]

In this example, inject-connection and auth-required are application-specific interceptors. body-params is a Pedestal function that returns an interceptor. view-user is a request-handling function.

When a request matches this route, the whole vector of interceptors gets pushed onto the context.

Common interceptors

The "terse" syntax uses hierarchically nested routes to reuse interceptors on subtrees. The table based syntax gives up that feature, but still allows you to compose interceptors:

;; Make a var with the common stuff
(def common-interceptors [inject-connection auth-required (body-params/body-params)])

;; inside a call to table-routes
["/user/:user-id/private" :post (conj common-interceptors view-user)]

This leaves your code in charge of composing interceptors using ordinary Clojure data manipulation.

Constraints

As a convenience, you can supply a map of constraints, in the form of regular expressions, that must match in order for the whole route to match. This handles that case from before, where we wanted to say that user IDs must be numeric.

You tell the router about constraints by supplying a map from parameter name to regular expression:

["/user/:user-id" :get view-user :constraints {:user-id #"[0-9]+"}]

Notice the :constraints keyword. That is required to tell the router that the following map is to be treated as constraints.

Like the interceptor vector, the constraint map is just data. Feel free to build it up however you like…​ it doesn’t have to be a map literal in the route vector:

(def numeric #"[0-9]+")
(def user-id {:user-id numeric})

["/user/:user-id" :get  view-user   :constraints user-id]
["/user/:user-id" :post update-user :constraints user-id]

Considering Constraints

The thing about constraints is that they are not used to distinguish between otherwise identical routes. They are not for disambiguation. Rather, constraints are used to reject requests match the rest of the route but don’t match the constraints.

What happens after such a rejection depends on which router is being used.

Router Behavior on Failed Constraint

Prefix tree

Abort and return a 404 response

Map tree

Abort and return a 404 response

Linear search

Continue searching the remaining routes

Be cautious about using path constraints.

Experience has shown that constraints result in a worse experience for users of your API. Generally, it is better to validate path and query parameters inside your route (in an interceptor or handler for the specific route) so that proper errors could be returned to the client, rather than a generic, and perhaps misleading, 404 NOT FOUND response.

As an alternative to using path constraints, you could define a validating interceptor:

(def user-id-validator
  (interceptor
     {:name ::user-id-validator
      :enter (fn [context]
               (if (re-matches #"[0-9]+" (get-in context [:request :path-params :user-id]))
                 context
                 (assoc context
                   :response {:status 400
                              :body "user-id must be numeric"})))}))

This interceptor could be placed into the interceptor list for any route that has a user-id in the path, and will abort any request (with a 400 Bad Request status code) if the user-id is not numeric.

Route names

Every route must have a unique name. Pedestal uses route names for the flip side of route matching: URL generation. You can supply a route name in the route vector:

["/user" :get view-user :route-name :view-user-profile]

A route name must be a keyword. Most commonly, it is a namespace-qualified keyword.

The route name comes before :constraints, so if you have both, the order is as follows

  1. Path - String

  2. Verb - keyword (e.g., :get)

  3. Interceptors - vector

  4. Route name clause (:route-name your-route-name)

  5. Constraints clause (:constraints constraint-map)

Default Route Names

You’ll notice that most of the examples above omit the :route-name clause. When there is no explicit route name, Pedestal will pick one for you. It uses the :name of the last interceptor in the interceptor vector.

When you reuse interceptors, or when you supply a handler function as the final interceptor, you’ll need to explicitly specify the route name.

Using Route Names to Distinguish Handlers

Suppose you have a single interceptor or handler that deals with multiple verbs on the same path. Maybe it’s a general API endpoint function or a function created by another library. If you just try to make multiple rows in a table, you will get errors:

;;; This won't work in table syntax. Both rows get the same automatic
;;; route name.
["/users" :get user-api-handler]
["/users" :post user-api-handler]

You have a couple of options. To stick with table syntax, you can use route names to distinguish the rows:

["/users" :get  user-api-handler :route-name :users-view]
["/users" :post user-api-handler :route-name :user-create]

The route names are enough to make each row unique.

With terse syntax, one path has a map that allows multiple verbs. Each verb can use the same handler as long as they specify different route names:

["/users" {:get user-api-handler
           :post [:user-create user-api-handler]}]

Generating URLs

In addition to routing, route tables are also used for URL generation. You can request a URL for a given route by name and specify parameter values to fill in. This section describes URL generation, starting with how routes are named.

URL generation

The url-for-routes function takes the parsed route table and returns a URL generating function. The generator accepts a route name and optional arguments and returns a URL that can be used in a hyperlink.

(def app-routes
   (table/table-routes
     {}
     [["/user"                   :get  user-search-form]
      ["/user/:user-id"          :get  view-user        :route-name :show-user-profile]
      ["/user/:user-id/timeline" :post post-timeline    :route-name :timeline]
      ["/user/:user-id/profile"  :put  update-profile]]))

(def url-for (route/url-for-routes app-routes))

(url-for :user-search-form)
;; => "/user"

(url-for :view-user :params {:user-id "12345"})
;; => "/user/12345"

Any leftover entries in the :params map that do not correspond to path parameters get turned into query string parameters. If you want more control, you can give the generator the specific arguments :path-params and :query-params.

Request-specific URL generation

The url-for-routes function provides a global URL generator. Within a single request, the request map itself can provide a URL generator. This generator allows you to create absolute or relative URLs depending on how the request was matched.

When the routing interceptor matches a request to a route, it creates a new URL generator function that closes over the request map. This function is stored in three places:

  • In the context map, using the :url-for key

  • In the request map (inside the context), using the same :url-for key

  • In a private dynamic variable in the io.pedestal.http.route namespace

The private variable allows the url-for function to operate from any interceptor (or handler) code.

Verb smuggling

url-for only returns URLs. The function form-action-for-routes takes a route table and returns a function that accepts a route-name (and optional arguments) and returns a map containing a URL and an HTTP verb.

(def form-action (route/form-action-for-routes app-routes))

(form-action :timeline :params {:user-id 12345})
;; => {:method "post", :action "/user/:user-id/timeline"}

A form action function will (by default) convert verbs other than GET or POST to POST, with the actual verb added as a query string parameter named _method:

(form-action :update-profile :params {:user-id 12345})
;; => {:method "post", :action "/user/12345/profile?_method=put"}

This behavior can be disabled (or enabled for url-for functions) and the query string parameter name can be changed. All of these settings can be modified when an url-for or form-action function is created or when it is invoked.

Using Routes in a Service

Up until now, we’ve looked at individual routes or a small handful of them. Now let’s see how to connect them to a Pedestal service.

Well, that means we probably need a service to put them in. Time to fire up leiningen. Pedestal provides a leiningen template to generate new projects. Find a nice spot to keep your work and run

The Pedestal Leiningen template has changed over time and may not exactly match this.
$ lein new pedestal-service myapp
Retrieving pedestal-service/lein-template/0.5.1/lein-template-0.5.1.pom from clojars
Retrieving pedestal-service/lein-template/0.5.1/lein-template-0.5.1.jar from clojars
Generating a pedestal-service application called myapp.
$ cd myapp
$ ls
Capstanfile Dockerfile  README.md   config      project.clj src         test

Looks like the template generated some good stuff. project.clj defines the new service’s dependencies and build instructions. Dockerfile lets you package this service as a Docker container. Capstanfile is a similar thing to compile this service into a unikernel application with OSv.

For now, the parts we really want to see are under src/myapp.

$ ls src/myapp
server.clj  service.clj

These two source files define a production-ready Pedestal service. All it serves right now as an "About" page, so we need to make some improvements before we ship it.

server.clj supplies the main entry point. service.clj defines the behavior of the service itself. These are starting points, though. Over time, you will certainly add more files and factor behavior out of these two. For now, we need to open up service.clj and make some changes.

Here is what the template generated for us (as of version 0.5.1, at least. Details may differ slightly in yours.)

src/myapp/service.clj
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]})

We will make this look more like the route table from Your First API.

src/myapp/service.clj
(def routes
   #{["/todo"                 :post   [db-interceptor list-create]]
     ["/todo"                 :get    echo :route-name :list-query-form]
     ["/todo/:list-id"        :get    [entity-render db-interceptor list-view]]
     ["/todo/:list-id"        :post   [entity-render list-item-view db-interceptor list-item-create]]
     ["/todo/:list-id/:item"  :get    [entity-render list-item-view]]
     ["/todo/:list-id/:item"  :put    echo :route-name :list-item-update]
     ["/todo/:list-id/:item"  :delete echo :route-name :list-item-delete]})

Notice the use of :list-id and :item to extract parts of the URL. Right now, each of those can contain any sequence of characters. Anything at all. Suppose we need to restrict them to numbers. That means we need to add constraints.

src/myapp/service.clj
(def numeric #"[0-9]+")
(def url-rules {:list-id numeric :item numeric})

(def routes
   #{["/todo"                 :post   [db-interceptor list-create]]
     ["/todo"                 :get    echo :route-name :list-query-form]
     ["/todo/:list-id"        :get    [entity-render db-interceptor list-view]                       :constraints url-rules]
     ["/todo/:list-id"        :post   [entity-render list-item-view db-interceptor list-item-create] :constraints url-rules]
     ["/todo/:list-id/:item"  :get    [entity-render list-item-view]                                 :constraints url-rules]
     ["/todo/:list-id/:item"  :put    echo :route-name :list-item-update                             :constraints url-rules]
     ["/todo/:list-id/:item"  :delete echo :route-name :list-item-delete :constraints url-rules]})

Here we’ve added a constraints clause to each of the routes with parameters. To make sure all the routes obey the same rules, we can factor those rules out into a value that we attach to each route.

One thing we haven’t demonstrated so far is the ability to match all of a URL’s string. You might do this if you need to create a URL hierarchy with arbitrary depth. That is the purpose of a wildcard URL like this:

src/myapp/service.clj
(def routes
   #{["/org/*todos" :get   [parse-hierarchy lookup-list]]})

Maybe this is for a manager to see every single TODO ever assigned to anyone in the organization. Seems perfectly reasonable.

This wildcard route will match anything that starts with /org/. Interceptors will receive a request with the path parameter :todos bound to everything that followed /org/. It is up to those interceptors to parse and interpret the URL.

Restrictions on Wildcards and Path Parameters

The map tree router is by far the fastest router in Pedestal. Part of how it gets that speed, though, is by forbidding the use of dynamic path segments like wildcards and path parameters.

If your routes include any of them, then even if you request the map tree router, Pedestal will fall back to using the (still pretty fast) prefix tree router.

Wrapping Up

This guide covered route definitions, from the most basic possible case all the way to the most complex. It demonstrated the use of the table routing syntax and showed how to use them in a real service. Finally, it demonstrated the use of wildcard routes and discussed when to use them and their tradeoffs.

For more details, see the Routing Quick Reference.