10 min read

In this article by Ryan Baldwin, the author of Clojure Web Development Essentials, however, we will start building our application, creating actual endpoints that process HTTP requests, which return something we can look at. We will:

  • Learn what the Compojure routing library is and how it works
  • Build our own Compojure routes to handle an incoming request

What this chapter won’t cover, however, is making any of our HTML pretty, client-side frameworks, or JavaScript. Our goal is to understand the server-side/Clojure components and get up and running as quickly as possible. As a result, our templates are going to look pretty basic, if not downright embarrassing.

(For more resources related to this topic, see here.)

What is Compojure?

Compojure is a small, simple library that allows us to create specific request handlers for specific URLs and HTTP methods. In other words, “HTTP Method A requesting URL B will execute Clojure function C. By allowing us to do this, we can create our application in a sane way (URL-driven), and thus architect our code in some meaningful way.

For the studious among us, the Compojure docs can be found at https://github.com/weavejester/compojure/wiki.

Creating a Compojure route

Let’s do an example that will allow the awful sounding tech jargon to make sense. We will create an extremely basic route, which will simply print out the original request map to the screen. Let’s perform the following steps:

  1. Open the home.clj file.
  2. Alter the home-routes defroute such that it looks like this:
    (defroutes home-routes
      (GET "/" [] (home-page))
      (GET "/about" [] (about-page))
      (ANY "/req" request (str request)))
  3. Start the Ring Server if it’s not already started.
  4. Navigate to http://localhost:3000/req.

It’s possible that your Ring Server will be serving off a port other than 3000. Check the output on lein ring server for the serving port if you’re unable to connect to the URL listed in step 4.

You should see something like this:

Clojure Web Development Essentials

Using defroutes

Before we dive too much into the anatomy of the routes, we should speak briefly about what defroutes is. The defroutes macro packages up all of the routes and creates one big Ring handler out of them. Of course, you don’t need to define all the routes for an application under a single defroutes macro. You can, and should, spread them out across various namespaces and then incorporate them into the app in Luminus’ handler namespace. Before we start making a bunch of example routes, let’s move the route we’ve already created to its own namespace:

  1. Create a new namespace hipstr.routes.test-routes (/hipstr/routes/test_routes.clj) . Ensure that the namespace makes use of the Compojure library:
    (ns hipstr.routes.test-routes
      (:require [compojure.core :refer :all]))
  2. Next, use the defroutes macro and create a new set of routes, and move the /req route we created in the hipstr.routes.home namespace under it:
    (defroutes test-routes
      (ANY "/req" request (str request)))
  3. Incorporate the new test-routes route into our application handler. In hipstr.handler, perform the following steps:
    1. Add a requirement to the hipstr.routes.test-routes namespace:
      (:require [compojure.core :refer [defroutes]]
        [hipstr.routes.home :refer [home-routes]]
        [hipstr.routes.test-routes :refer [test-routes]]
        …)
    2. Finally, add the test-routes route to the list of routes in the call to app-handler:
      (def app (app-handler
        ;; add your application routes here
        [home-routes test-routes base-routes]

We’ve now created a new routing namespace. It’s with this namespace where we will create the rest of the routing examples.

Anatomy of a route

So what exactly did we just create? We created a Compojure route, which responds to any HTTP method at /req and returns the result of a called function, in our case
a string representation of the original request map.

Defining the method

The first argument of the route defines which HTTP method the route will respond to; our route uses the ANY macro, which means our route will respond to any HTTP method. Alternatively, we could have restricted which HTTP methods the route responds to by specifying a method-specific macro. The compojure.core namespace provides macros for GET, POST, PUT, DELETE, HEAD, OPTIONS, and PATCH.

Let’s change our route to respond only to requests made using the GET method:

(GET "/req" request (str request))

When you refresh your browser, the entire request map is printed to the screen, as we’d expect. However, if the URL and the method used to make the request don’t match those defined in our route, the not-found route in hipstr.handler/base-routes is used. We can see this in action by changing our route to listen only to the POST methods:

(POST "/req" request (str request))

If you try and refresh the browser again, you’ll notice we don’t get anything back.
In fact, an “HTTP 404: Page Not Found” response is returned to the client. If we POST to the URL from the terminal using curl, we’ll get the following expected response:

# curl -d {} http://localhost:3000/req
{:ssl-client-cert nil, :go-bowling? "YES! NOW!", :cookies {}, :remote-addr "0:0:0:0:0:0:0:1", :params {}, :flash nil, :route-params {}, :headers {"user-agent" "curl/7.37.1", "content-type" "application/x-www-form-urlencoded", "content-length" "2", "accept" "*/*", "host" "localhost:3000"}, :server-port 3000, :content-length 2, :form-params {}, :session/key nil, :query-params {}, :content-type "application/x-www-form-urlencoded", :character-encoding nil, :uri "/req", :server-name "localhost", :query-string nil, :body #<HttpInput org.eclipse.jetty.server.HttpInput@38dea1>, :multipart-params {}, :scheme :http, :request-method :post, :session {}}

Defining the URL

The second component of the route is the URL on which the route is served. This can be anything we want and as long as the request to the URL matches exactly, the route will be invoked. There are, however, two caveats we need to be aware of:

  • Routes are tested in order of their declaration, so order matters.
  • The trailing slash isn’t handled well. Compojure will always strip the trailing slash from the incoming request but won’t redirect the user to the URL without the trailing slash. As a result an HTTP 404: Page Not Found response is returned. So never base anything off a trailing slash, lest ye peril in an ocean of confusion.

Parameter destructuring

In our previous example we directly refer to the implicit incoming request and pass that request to the function constructing the response. This works, but it’s nasty. Nobody ever said, I love passing around requests and maintaining meaningless code and not leveraging URLs, and if anybody ever did, we don’t want to work with them. Thankfully, Compojure has a rather elegant destructuring syntax that’s easier to
read than Clojure’s native destructuring syntax.

Let’s create a second route that allows us to define a request map key in the URL, then simply prints that value in the response:

(GET "/req/:val" [val] (str val))

Compojure’s destructuring syntax binds HTTP request parameters to variables of the same name. In the previous syntax, the key :val will be in the request’s :params map. Compojure will automatically map the value of {:params {:val…}} to the symbol val in [val]. In the end, you’ll get the following output for the URL http://localhost:3000/req/holy-moly-molly:

Clojure Web Development Essentials

That’s pretty slick but what if there is a query string? For example, http://localhost:3000/req/holy-moly-molly!?more=ThatsAHotTomalle. We can simply add the query parameter more to the vector, and Compojure will automatically bring it in:

(GET "/req/:val" [val more] (str val "<br>" more))

Clojure Web Development Essentials

Destructuring the request

What happens if we still need access to the entire request? It’s natural to think we could do this:

(GET "/req/:val" [val request] (str val "<br>" request))

However, request will always be nil because it doesn’t map back to a parameter key of the same name. In Compojure, we can use the magical :as key:

(GET "/req/:val" [val :as request] (str val "<br>" request))

This will now result in request being assigned the entire request map, as shown in the following screenshot:

Clojure Web Development Essentials

Destructuring unbound parameters

Finally, we can bind any remaining unbound parameters into another map using &. Take a look at the following example code:

(GET "/req/:val/:another-val/:and-another"
  [val & remainders] (str val "<br>" remainders))

Saving the file and navigating to http://localhost:3000/req/holy-moly-molly!/what-about/susie-q will render both val and the map with the remaining unbound keys :another-val and :and-another, as seen in the following screenshot:

Clojure Web Development Essentials

Constructing the response

The last argument in the route is the construction of the response. Whatever the third argument resolves to will be the body of our response. For example, in the following route:

(GET "/req/:val" [val] (str val))

The third argument, (str val), will echo whatever the value passed in on the URL is. So far, we’ve simply been making calls to Clojure’s str but we can just as easily call one of our own functions. Let’s add another route to our hipstr.routes.test-routes, and write the following function to construct its response:

(defn render-request-val [request-map & [request-key]]
  "Simply returns the value of request-key in request-map,
  if request-key is provided; Otherwise return the request-map.
  If request-key is provided, but not found in the request-map,
  a message indicating as such will be returned."
(str (if request-key
        (if-let [result ((keyword request-key) request-map)]
          result
          (str request-key " is not a valid key."))
        request-map)))
(defroutes test-routes
  (POST "/req" request (render-request-val request))
  ;no access to the full request map
  (GET "/req/:val" [val] (str val))
  ;use :as to get access to full request map
  (GET "/req/:val" [val :as full-req] (str val "<br>" full-req))
  ;use :as to get access to the remainder of unbound symbols
  (GET "/req/:val/:another-val/:and-another" [val & remainders]
    (str val "<br>" remainders))
  ;use & to get access to unbound params, and call our route
  ;handler function
  (GET "/req/:key" [key :as request]
    (render-request-val request key)))

Now when we navigate to http://localhost:3000/req/server-port, we’ll see the value of the :server-port key in the request map… or wait… we should… what’s wrong?

Clojure Web Development Essentials

If this doesn’t seem right, it’s because it isn’t. Why is our /req/:val route getting executed? As stated earlier, the order of routes is important. Because /req/:val with the GET method is declared earlier, it’s the first route to match our request, regardless of whether or not :val is in the HTTP request map’s parameters. Routes are matched on URL structure, not on parameters keys. As it stands right now, our /req/:key will never get matched. We’ll have to change it as follows:

;use & to get access to unbound params, and call our route handler 
function (GET "/req/:val/:another-val/:and-another" [val & remainders]   (str val "<br>" remainders))   ;giving the route a different URL from /req/:val will ensure its
  execution   (GET "/req/key/:key" [key :as request] (render-request-val
  request key)))

Now that our /req/key/:key route is logically unique, it will be matched appropriately and render the server-port value to screen. Let’s try and navigate to http://localhost:3000/req/key/server-port again:

Clojure Web Development Essentials

Generating complex responses

What if we want to create more complex responses? How might we go about doing that? The last thing we want to do is hardcode a whole bunch of HTML into a function, it’s not 1995 anymore, after all. This is where the Selmer library comes to the rescue.

Summary

In this article we have learnt what Compojure is, what a Compojure routing library is and how it works. You have also learnt to build your own Compojure routes to handle an incoming request, within which you learnt how to use defroutes, the anatomy of a route, destructuring parameter and how to define the URL.

Resources for Article:

Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here