Routing with Compojure
What's Compojure
Compojure is a routing library for Ring interfaces. Compojure realise readable routing by the macro instead of the data. The components of Compojure are :
namespace | usage |
---|---|
compojure.core | provides some basic macros for routing |
compojure.route | provides some useful functions returning response |
compojure.coercions | provides route parameters |
compojure.response | provides render function which add some available types for :body |
You are going to use compojure.core
and compojure.route
for now.
Routing with Compojure
Firstly, add Compojure to the dependencies in project.clj
.
:dependencies [[org.clojure/clojure "1.10.0"] [ring "1.7.1"] [compojure "1.6.1"]]
After that, you can restart your REPL, then Leiningen would solve the dependencies automatically.
Next, let's try to make some routings with Compojure. Add two Compojure namespaces, namely compojure.core
and compojure.route
to your current namespace.
(ns example-clj.core (:require [compojure.core :refer [defroutes context GET]] [compojure.route :as route] [ring.adapter.jetty :as server] [ring.util.response :as res]))
defroutes
,context
, andGET
are macros which were defined incompojure.core
.ring.util.response
has a bunch of functions to make various responses easily.
You can basically route with the macros in the compojure.core
like GET
and POST. Besides that, you can use various functions in
compojure.route`.
(defn html [res] (res/content-type res "text/html; charset=utf-8"))
- Defining a function called "html" that takes HTTP response as a map.
res/content-type
takes a response-map and a value which will be a Content-Type in HTTP header, and set that value toContent-Type
key in header in response-map.
Good.
Well, now you should defn the home
and index
with their views and html functions.
;; > 1 (defn home [req] (-> (home-view req) res/response html)) ;; > 2 (defn index [req] (-> (index-view req) res/response html))
- The function
home
takes request-map.->
is called thread-first macro which make code more readable by removing nesting.
The above definition is equal to :
(defn home [req] (html (res/response (home-view req))))
- The definition of
index
almost the same ashome
. You know, thread-macro is an effective way to avoid the deep nesting.
More about Compojure
Here, we will take some examples and look around Compojure more in detail. As you can see from the code so far, the syntax for routing by Compojure is like this :
(GET "/" req home)
This kind of routing definition takes the request-map(req
) and returns the Ring handler which returns response-map. It depends on the definitions of the HTTP method and the path whether Compojure execute the Ring handler or not. In this case, if the HTTP request is GET and the path is "/", then the Ring handler is executed.
The compojure.core
namespace has some macros which have same names of HTTP methods in Ring such as :
GET
POST
PUT
DELETE
OPTIONS
PATCH
HEAD
These macros can be used in actuality and when anything is fine, you can use ANY
macro.
The macros like GET
takes 2 or more args. The paths for the first, the bindings for the second, and the responses generated by
the binding for each after the third.
You can define the paths basically as string like "/"
or "/index"
. In addition to this, you can specify special form including route parameter. In this way, any string can be accepted as a path in the part of: id up to the next "/" or ".". When you are going to use only numbers as a path, it is good to use regular expression like this :
(GET ["/index/:id" :id #"[0-9]+"] req index-show)
The routing definitions can be grouped together by route
function. route
convert some routing definitions as handler to one Ring handler.
(def handler (routes ;; compojure.core/routes (GET "/" req home) (GET "/index" req index) (GET "/index/:id" [id] (index-show id))))
Each routing definitions are tested from top to bottom. If the route
find the handler that returns some value (= not nil
) then the handler will be executed. In addition to this, route
can also contain the handler that is grouped by another route
. So you can do this :
;; > 1 (defroutes main-routes (GET "/" req home) (route/not-found "<h1>404 page not found</h1>")) ;; > 2 (defroutes index-routes (context "/index" req (GET "/" req index) (GET "/new" req index-new) (context "/:id" [id] (GET "/" req (index-show id))))) ;; > 3 (defroutes handler (routes index-routes main-routes))
Pay attention to the new function named context
. This context
groups together the common part of the paths used in macros like GET
. The context
takes path, binding, and routing definition as first, second and third arguments respectively.
defroute
definesmain-routes
.GET
macro takes"/"
as a path,req
as a binding, andhome
handler as a routing definition.defroute
definesindex-routes
.context
takes"/index"
as a path,req
as a binding, and it takes three elements after(GET "/" req index)
as routing definitions.defroute
defines handler. The order is a matter as themain-routes
uses thenot-found
function that must return a non-nil value.
Summary
In this post, you have learned how to build the routing definition and wrap it by macros with Compojure. It's essential for every backend-web-development scenes to make routing correctly. Next, you need to get used to Hiccup, a library that provides some template DSL for generating HTML dynamically.