Compojure & Clout Tutorial

A super cool routing library for Clojure’s Ring web framework

Once of the great philosophies in the Clojure community is to build small independent specialised libraries which can be nicely composed to solve problems. So rather than build solutions from massive complex web frameworks with various tradeoffs and opinionated solutions, Clojurians’ prefer to pick the right tool for the task at hand.

There are a few different options out there for routing, including gudu, bidi, Reitit, and playnice, as illustrated in 10+ Frameworks For Building Web Applications In Clojure by Nordic API’s. However, in this article we will be looking at Compojure — a small routing library that works nicely with Ring, which also happens to be the most widely used routing library by Clojure developers.

We will look at at how Compojure matches ring requests to an appropriate route to start with, before going on to examine how we can pass and parse query string and path parameters using Ring’s parameters middleware.

Compojure helps us to easily create Ring request handlers

Compojure is often used with Ring, Clojures popular http server library. Its purpose is to take a ring request map, match a route and call the appropriate request handler. The Compojure repo provides the following example:

The defroutes macro is part of the Compojure library and returns a function, app-routes, which we can use for routing.

As you can see from the example, we can use the defroutes macro to define route handlers for our application. Specifically, the macro actually combines all of the route handlers into a single ring handler.

Each handler passed to defroutes uses clout, a small library written by the Compojure team to match requests. Let’s first take a look at some of these handlers to get a feel for what clout does for us.

Understanding Clout

Clout is a pattern matching library for Ring requests, and uses the same routing syntax as Ruby on Rails and Sinatra. We can define routes by using the appropriately named functions, GET, PUT, POST, PATCH, DELETE from compojure.core. The job of each handler, is to match the appropriate ring request map and return a response map.

One we have defined an example route handler using clout, we can then pass in a ring request map, and the example route will respond with the appropriate response.

As you can see, our example route defined a uri path to handle any HTTP GET requests at the top level. Given that our example ring request map matched both the :uri and :request-method of our handler, it returned a 200 response with the default headers.

Now let’s try the same handler with a different http verb, POST:

The handler returned nil, as there was no match. Similarly, if we passed in a different :uri, no match would be found:

We can also define wildcard routes:

If you take a look at the last argument to our GET function we defined for our example-route, you can see that it returns a string.

We can also pass in a function as the last argument instead, which will then be called if our example-route is matched.

In the above example, we get the following output at the repl:

Our handler still returns the status, default headers and body response we expect, but what is interesting is the fact that our dog-handler-method is called with a map containing the full uri, request-method, route-params and params.

Adding query parameters to a request

Now that we have access to parameters in the request map, let’s try passing some query parameters into our dogs-route:

Woah, it looks like our uri of our request map no longer matches our /dogs pattern we defined in our handler due to the additional query params. This is because ring normally extracts these query parameters into a :query-string key automatically when creating the request map, before our handler is called.

Let’s now fix this request map to be more like a proper ring request map by including the :query-string key.

Cool, so it turns out that our handler does have access to query string parameters if they are passed in via the :query-string key in the request map. However, they are still in the string format, so at the moment are not very usable. To fix this, we would typically use the ring parameters middleware.

This is getting better, we now have a :params key with a map of our query parameters.

When applied to a handler, the parameter middleware adds three new keys to the request map:

  • :query-params - A map of parameters from the query string
  • :form-params - A map of parameters from submitted form data
  • :params - A merged map of all parameters

Unfortunately these key value pairs are strings, which makes the parameters map a bit difficult to use, but at least we have access our query params, so we can redefine our dogs handler as follows:

We used the clojure.walk/keywordize-keys fn to turn our :params map from:

As you might guess, this is quite a common task in Clojure, so there’s some other ring middleware to do this for us!

So now we can remove the keywordize-keys functions from our handler:

Providing path variables

We can also assign path variables to keywords as follows:

You will note that the keys defined in our path and query-string examples both end up in the :path key of our request map.

By now you might be wondering what the empty vector in the second argument of the GET function is for? Well, it’s for defining local arguments based on the keywords we defined in our path string. So we could refactor our dogs-handler-method as follows:

This looks a lot nicer and is easier to read. Hopefully this article helped to explain a little bit about how the Compojure handlers work.

If you are interested in learning more about Compojure’s route matching capabilities, then be sure to check out the more advanced pattern matching capabilities built into Clout and Compojure on the Clout Repo:

Functional Programming for Humans

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store