ハツカネズミの恋

Lisp のもろもろ,おぼえがき

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]))
  1. defroutes,context, and GET are macros which were defined in compojure.core.
  2. 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 incompojure.route`.

(defn html [res]
  (res/content-type res "text/html; charset=utf-8"))
  1. Defining a function called "html" that takes HTTP response as a map.
  2. res/content-type takes a response-map and a value which will be a Content-Type in HTTP header, and set that value to Content-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))
  1. 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))))
  1. The definition of index almost the same as home. 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.

  1. defroute defines main-routes. GET macro takes "/" as a path, req as a binding, and home handler as a routing definition.
  2. defroute defines index-routes. context takes"/index" as a path, req as a binding, and it takes three elements after (GET "/" req index) as routing definitions.
  3. defroute defines handler. The order is a matter as the main-routes uses the not-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.