Om Next remotes feature a single API endpoint that accepts both GET and POST request meth­ods. The Om Next parser looks at the request para­me­ter­s—not the request method—to deter­mine 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 solu­tion 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 help­fully includes exam­ples of all three, with the the terse and map-based versions commented out.

The ta­ble routing syntax was intro­duced with the 0.5.0 release of Pedestal. The routing documentation explains why it is the default:

[The ta­ble syntax] is more wordy and has some repe­ti­tion 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 hierarchic 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.

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 corre­sponding route definitions.1

(defn about-page
  (ring-resp/response (format "Clojure %s - served from %s"
                              (route/url-for ::about-page))))

(defn home-page
  (ring-resp/response "Hello World!"))

;; Defines "/" and "/about" routes with their associated :get handlers.
;; The interceptors defined after the verb map (e.g., {:get home-page}
;; apply to / and its children (/about).
(def common-interceptors [(body-params/body-params) http/html-body])

;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]})

;; Map-based routes
;(def routes `{"/" {:interceptors [(body-params/body-params) http/html-body]
;                   :get home-page
;                   "/about" {:get about-page}}})

;; Terse/Vector-based routes
;(def routes
;  `[[["/" {:get home-page}
;      ^:interceptors [(body-params/body-params) http/html-body]
;      ["/about" {:get about-page}]]]])

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 reas­suring 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.

(defn api-handler
  "Generic api-handler example"
  [{:keys [request-method] :as request}]
    (format "Thank you for requesting API! [%s]\n" request-method)))

Now to define the GET and POST routes for a new /api endpoint. I first tried adding two entries, one for each method.

(def routes
  #{["/" :get (conj common-interceptors `home-page)]
    ["/about" :get (conj common-interceptors `about-page)]
    ["/api" :get (conj common-interceptors `api-handler)]
    ["/api" :post (conj common-interceptors `api-handler)]})

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}
(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.

(defn api-post-hander [req]
  (api-handler req))

(def routes
   #{["/" :get (conj common-interceptors `home-page)]
     ["/about" :get (conj common-interceptors `about-page)]
     ["/api" :get (conj common-interceptors `api-handler)]
     ["/api" :post (conj common-interceptors `api-post-handler)]})

The lein run-dev process starts up success­fully. 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 func­tion doesn’t feel right. Aliasing the api-han­dler instead of creating a new func­tion works as well:

(def api-post-handler api-handler)

More succinct, but it still feels like a hack.

I looked at the source to see how the ta­ble routes are parsed, hoping there might be some undoc­u­mented syntax for spec­i­fying multiple methods for the same handler. I didn’t find one, but I saw refer­ences to :route-name and was reminded of the earlier AssertionError. So I looked at the routing documentation for refer­ences to :route-name.

Default Route Names

You’ll notice that none of the exam­ples before now have a :route-name section. If you don’t explic­itly specify a route name, Pedestal will pick one for you. It uses the :name of the last inter­ceptor in the inter­ceptor vector (after resolving func­tions to inter­ceptors.) Most of the time, you’ll have different handler func­tions in that terminal posi­tion. But, if you reuse an inter­ceptor as the final step of the chain, you will have to assign unique route names to distin­guish them.

Confir­ma­tion! I added a route name for the POST route.

(def routes
  #{["/" :get (conj common-interceptors `home-page)]
    ["/about" :get (conj common-interceptors `about-page)]
    ["/api" :get (conj common-interceptors `api-handler)]
    ["/api" :post (conj common-interceptors `api-handler) :route-name :api-post]})

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 devel­op­ment. I’ve docu­mented my devel­op­ment process here as honestly as I can recall. If I had read the routing docu­men­ta­tion completely and under­stood it, I wouldn’t have had this issue. However, I think it’s useful looking at devel­op­ment process as well, and I suspect I’m not the only one who dives into new mate­rial without fully reading the docu­men­ta­tion.

Looking at the map and terse route syntax, I couldn’t find exam­ples or docu­men­ta­tion where the route name is explic­itly spec­i­fied. I looked at the terse route defi­n­i­tion parser source and saw where a keyword could be inter­preted as a route name. I then looked at the route tests, and found examples spec­i­fying a handler/in­ter­ceptor chain as a vector where a first-ele­ment keyword is inter­preted as the route name. Again, I named the POST route :api-post.

;; Terse/Vector-based routes
(def routes
  `[[["/" {:get home-page}
      ^:interceptors [(body-params/body-params) http/html-body]
      ["/about" {:get about-page}]
      ["/api" {:get api-handler
               :post [:api-post api-handler}]]])

And it worked!

I did come across a refer­ence to explicit route names in the docu­men­ta­tion 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 spec­i­fied as the value of a given HTTP verb for a given route. Explicit route names take prece­dence over implicit names. For routes that cannot be given an implicit name, an explicit name must be provided or an excep­tion 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

Again, I found was after I figured out how to do it from looking at the source and tests and was looking for docu­men­ta­tion that I might have missed when preparing this post. I know the Pedestal main­tainers do their best to provide docu­men­ta­tion. I just don’t always find it. There’s my devel­op­ment process for you.

The same syntax works for naming map routes.

;; Map-based routes
(def routes
  `{"/" {:interceptors [(body-params/body-params) http/html-body]
         :get home-page
         "/about" {:get about-page}
         "/api" {:get api-handler
                 :post [:api-post api-handler]}})

There you have it. Explic­itly naming routes allows you to use the same handler for multiple routes, with exam­ples of naming routes in table, map, and terse route syntax.

  1. As of this writing, the service.clj file contains typos in the commented-out route definitions. I’ve fixed them here, renaming bootstrap to http. This has already been fixed in master. ↩︎