Naming Routes in Pedestal
Om Next remotes feature a single API endpoint that accepts both GET
and
POST
request methods. The Om Next parser looks at the request
parameters—not the request method—to determine what action to take; only a
single handler calling the parser is needed. How do you define Pedestal routes
for the same endpoint with two request methods but only a single handler?
Just starting out using Pedestal, the solution wasn’t clear to me.
Pedestal supports multiple ways to define routes: terse, map-based,
and table. When you generate a new Pedestal service using
leiningen, the service.clj
file helpfully includes examples of
all three, with the the terse and map-based versions commented out.
The table routing syntax was introduced with the 0.5.0 release of Pedestal. The routing documentation explains why it is the default:
[The table syntax] is more wordy and has some repetition from row to row, but it also has some advantages:
- 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.)
- The table does not have hierarchic nesting, so the rows are independent.
- 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.
Here’s an out-of-the-box example.
> lein new pedestal-service route-ex
> cd route-ex
In src/route_ex/service.clj
we see two handlers, about-page
and home-page
,
and their corresponding route definitions.1
Let’s take a quick look at what the default set up gives us. In one shell:
> lein run-dev
And in another:
> curl localhost:8080
Hello World!
> curl localhost:8080/about
Clojure 1.8.0 - served from /about
Success! It’s reassuring to confirm it works before I start updating the code.
Here’s the new handler, standing in for an Om Next API endpoint. Note that it reports the request method in the response.
Now to define the GET
and POST
routes for a new /api
endpoint. I first
tried adding two entries, one for each method.
Kill the lein run-dev
process and re-run. This will fail on start-up.
> lein run-dev
Exception in thread "main" java.lang.AssertionError: Assert failed:
Route name or handler appears more than once in the route spec:
[#Interceptor{:name :io.pedestal.http.body-params/body-params}
#Interceptor{:name :io.pedestal.http/html-body}
route-ex.service/api-handler]
(nil? (seen-route-names rname)), compiling:(route_ex/server.clj:7:1)
The first time I read this error message I keyed in on the handler appears
more than once phrase, so I created an api-post-handler
that passed the
request through to api-handler
.
The lein run-dev
process starts up successfully. Do the requests work
as expected?
> curl localhost:8080/api
Thank you for requesting API! [:get]
> curl localhost:8080/api -X POST
Thank you for requesting API! [:post]
It works! But creating a dummy wrapper function doesn’t feel right. Aliasing the api-handler instead of creating a new function works as well:
More succinct, but it still feels like a hack.
I looked at the source to see how the table routes are parsed, hoping there
might be some undocumented syntax for specifying multiple methods for the same
handler. I didn’t find one, but I saw references to :route-name
and was
reminded of the earlier AssertionError
. So I looked at the
routing documentation for references to :route-name
.
Default Route Names
You’ll notice that none of the examples before now have a
:route-name
section. If you don’t explicitly specify a route name, Pedestal will pick one for you. It uses the:name
of the last interceptor in the interceptor vector (after resolving functions to interceptors.) Most of the time, you’ll have different handler functions in that terminal position. But, if you reuse an interceptor as the final step of the chain, you will have to assign unique route names to distinguish them.
Confirmation! I added a route name for the POST
route.
And it works!
> curl localhost:8080/api
Thank you for requesting API! [:get]
> curl localhost:8080/api -X POST
Thank you for requesting API! [:post]
A quick note on development. I’ve documented my development process here as honestly as I can recall. If I had read the routing documentation completely and understood it, I wouldn’t have had this issue. However, I think it’s useful looking at development process as well, and I suspect I’m not the only one who dives into new material without fully reading the documentation.
Looking at the map and terse route syntax, I couldn’t find examples or
documentation where the route name is explicitly specified. I looked at the
terse route definition parser source and saw where a keyword could be
interpreted as a route name. I then looked at the route
tests, and found examples specifying a
handler/interceptor chain as a vector where a first-element keyword is
interpreted as the route name. Again, I named the POST
route :api-post
.
And it worked!
I did come across a reference to explicit route names in the
documentation for the terse route syntax, as well as
an example of a route named :make-an-order
.
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]]]]])
Again, I found was after I figured out how to do it from looking at the source and tests and was looking for documentation that I might have missed when preparing this post. I know the Pedestal maintainers do their best to provide documentation. I just don’t always find it. There’s my development process for you.
The same syntax works for naming map routes.
There you have it. Explicitly naming routes allows you to use the same handler for multiple routes, with examples of naming routes in table, map, and terse route syntax.
- As of this writing, the
service.clj
file contains typos in the commented-out route definitions. I’ve fixed them here, renamingbootstrap
tohttp
. This has already been fixed in master. ↩︎