Sleeping Man

Some thoughts of when I am not sleeping.

Cleaner Compojure Routes

| Comments

Whenever writing compojure routes a common problem that we can see is repetition. For instance, let’s imagine a service that manages a meerkats zoo. In this context, we would probably have some routes for handling the meerkats. The code snippet below pictures this situation:

1
2
3
4
5
6
7
8
9
10
11
12
13
(ns meerkats.routes
  (:require
    [meerkats.controllers.meerkats :as meerkats-controller]
    [compojure.route               :as route]
    [compojure.core                :refer :all]))

(defroutes main-routes
  (GET    "/meerkats" [] (meerkats-controller/index))
  (GET    "/meerkats/:id" [id] (meerkats-controller/show id))
  (POST   "/meerkats" {params :params} (meerkats-controller/create params))
  (PATCH  "/meerkats/:id" {params :params} (meerkats-controller/update params))
  (DELETE "/meerkats/:id" [id] (meerkats-controller/destroy id))
  (ANY "*" [] (route/not-found "<h1>Page not found</h1>"))

There is a lot of repetition!

Let’s make this better with the context macro that compojure provides.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(ns meerkats.routes
  (:require
    [meerkats.controllers.meerkats :as meerkats-controller]
    [compojure.route               :as route]
    [compojure.core                :refer :all]))

(defroutes main-routes
  (context "/meerkats" []
    (GET    "/" [] (meerkats-controller/index))
    (GET    "/:id" [id] (meerkats-controller/show id))
    (POST   "/" {params :params} (meerkats-controller/create params))
    (PATCH  "/:id" {params :params} (meerkats-controller/update params))
    (DELETE "/:id" [id] (meerkats-controller/destroy id)))
  (ANY "*" [] (route/not-found "<h1>Page not found</h1>"))

This is better, most of the duplication is gone. On the other hand, what I like the most about this is that we are semantically saying that all of these resources belong to the same entity, the meerkats.

And there is more. As you could have noticed, the context macro provides a list of arguments, which could be used to capture all the routes that receive arguments in their URL. That would be available (through closure) for all the contained routes. This is particularly useful for nested resources.

In order to exemplify this, let’s say we can manage babies for a given meerkat. This could be done through contexts as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(ns meerkats.routes
  (:require
    [meerkats.controllers.meerkats :as meerkats-controller]
    [meerkats.controllers.babies   :as babies-controller]
    [compojure.route               :as route]
    [compojure.core                :refer :all]))

(defroutes main-routes
  (context "/meerkats" []
    (GET    "/" [] (meerkats-controller/index))
    (GET    "/:id" [id] (meerkats-controller/show id))
    (POST   "/" {params :params} (meerkats-controller/create params))
    (PATCH  "/:id" {params :params} (meerkats-controller/update params))
    (DELETE "/:id" [id] (meerkats-controller/destroy id))

    (context "/:meerkat-id/babies" [meerkat-id]
      (GET    "/" [] (babies-controller/index meerkat-id))
      (GET    "/:id" [id] (babies-controller/show meerkat-id id))
      (POST   "/" {params :params} (babies-controller/create meerkat-id params))
      (PATCH  "/:id" {params :params} (babies-controller/update meerkat-id params))
      (DELETE "/:id" {params :params} (babies-controller/destroy meerkat-id params)))
  (ANY "*" [] (route/not-found "<h1>Page not found</h1>"))

Comments