





















































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:
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.)
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.
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:
(defroutes home-routes (GET "/" [] (home-page)) (GET "/about" [] (about-page)) (ANY "/req" request (str request)))
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:
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:
(ns hipstr.routes.test-routes (:require [compojure.core :refer :all]))
(defroutes test-routes (ANY "/req" request (str request)))
(:require [compojure.core :refer [defroutes]] [hipstr.routes.home :refer [home-routes]] [hipstr.routes.test-routes :refer [test-routes]] …)
(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.
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.
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 {}}
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:
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:
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))
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:
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:
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?
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:
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.
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.
Further resources on this subject: