boolean·knot

technical blog

22 May 2015

Structuring Clojure Web Applications

The Clojure web ecosystem doesn’t provide as much guidance to newcomers as other languages. There are no large frameworks on the same scale or adoption as Ruby on Rails or Django, and in their absense, a common question arises: how do I structure a web application in Clojure?

I haven’t seen a satisfactory answer to this question, but that’s not because no-one’s bothered to document common wisdom. Rather, I believe it’s because we’re only now beginning to discover good solutions in Clojure to this rather thorny set of problems.

But before we begin, a brief note on the subject of complexity is needed. It’s impossible to talk about the merits of different solutions in Clojure without mentioning complexity, a word which Clojure has adopted to be a measure of interconnectivity in code. The more connections a variable or function has to the rest of the code base, the more complex it is. Rich Hickey’s Simple Made Easy talk introduces this subject, and I’d highly recommend watching if you haven’t already.

Configuration and connections

Most web applications have connections to external services, such as databases, queues, caches, and so forth. I’ve seen a number of methods for getting these connections to the code that responds to HTTP requests, but only one method that really makes sense, which is to use a closure:

(defn app [{:keys [db]}]
  (routes
   (GET "/" [] (show-index db))
   (route/not-found (io/resource "errors/404.html"))))

In the above example, we create a Ring handler by passing a configuration map to the app function, which uses Compojure to handle routing. The configuration may contain connections to external services, such as a database, which in this case has been assigned to the :db key.

By using a closure, we confine the configuration to a lexical scope. It’s less complex than a global variable, which touches everything, or even a dynamic binding, which reaches an arbitrary depth into the callstack. A closure is also neater than attaching the database connection to the request map; we need only destructure it once, rather than in every route.

Views and models

Many web frameworks separate the act of fetching data and formatting it, and this is a sound principle to apply in Clojure as well. I/O is a complex operation - after all, it can potentially affect any server in the world! If we can’t minimise this complexity, we can at least seek to isolate it.

For this reason, we’ll separate our show-index function in our previous example into two:

(defn fetch-index [db]
  {:products (fetch-products db)
   :orders   (fetch-orders db)})

(defn view-index [{:keys [products orders]}]
  ;; formatting goes here
  )

(defn app [{:keys [db]}]
  (routes
   (GET "/" [] (view-index (fetch-index db)))
   (route/not-found (io/resource "errors/404.html"))))

This provides a clean separation between the view and model, and also makes the functions that much easier to test. The view-index function is essentially pure, while the fetch-index function is deliberately minimal.

Mocks and protocols

Mocks are usually a symptom of a complected design, but occassionally they are useful for the parts of your application that are necessarily complex, such as connections to a database.

In order to make this process easier, we can wrap database operations in a protocol:

(defprotocol Database
  (fetch-products [db])
  (fetch-orders [db]))

(defrecord SqlDatabase [conn]
  Database
  (fetch-products [_]
    ;; fetch a list of products from the database
    )
  (fetch-orders [_]
    ;; fetch a list of orders from the database
    )

In our tests we can create a mock database that implements the Database protocol, and pass this to the app function. As an additional benefit, we also gain some measure of independence from the database; changing database can be done through reimplementing the protocol.

This approach works well with Stuart Sierra’s Component library.

Summary

We can sum all this up into three broad rules of thumb:

  1. Use closures to pass in configuration
  2. Isolate side-effects from functional code
  3. Use protocols to provide an interface to external services

While this is far from a complete solution to the question posed in the introduction, it’s perhaps the beginnings of one.