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:
home.clj
file.home-routes
defroute such that it looks like this:(defroutes home-routes (GET "/" [] (home-page)) (GET "/about" [] (about-page)) (ANY "/req" request (str request)))
http://localhost:3000/req
.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:
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]))
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)))
test-routes
route into our application handler. In hipstr.handler
, perform the following steps:hipstr.routes.test-routes
namespace:(:require [compojure.core :refer [defroutes]]
[hipstr.routes.home :refer [home-routes]]
[hipstr.routes.test-routes :refer [test-routes]]
…)
test-routes
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 and incorporated 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 component 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:
HTTP 404: Page Not Found
response is returned. So never base anything off a trailing slash, lest ye peril in an ocean of confusion.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.
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 component 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 component, (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 & to get access to the remainder of unbound symbols (GET "/req/:val/:another-val/:and-another" [val & remainders] (str val "<br>" remainders)) ;use :as 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:
18.118.205.235