boolean·knot

technical blog

29 May 2017

Building Web Services with Duct

If you’re new to the 0.9 alpha version of Duct, I suggest you take a detour and read Advancing Duct, which covers the new architecture of Duct more completely.

By contrast, this post won’t cover old ground, but instead focus on a more specific (but very common) use-case: web services. I’ll also introduce Ataraxy, a data-driving routing library that integrates well with Duct.

Let’s start by creating a new Duct project, and we’ll use the +api, +ataraxy and +sqlite profile hints to speed things up:

$ lein new duct-alpha foo +api +ataraxy +sqlite
Generating a new Duct project named foo...
Run 'lein duct setup' in the project directory to create local config files.

Let’s enter the project, and run the setup script to generate the local files that are kept out of source control:

$ cd foo
$ lein duct setup
Created profiles.clj
Created .dir-locals.el
Created dev/resources/local.edn
Created dev/src/local.clj

Next, we start a REPL:

$ lein repl

And initiate the system for development:

user=> (dev)
:loaded
dev=> (go)
:duct.server.http.jetty/starting-server {:port 3000}
:initiated

We can use another terminal to check that the server is running:

$  curl http://localhost:3000/
{"error":"not-found"}

We get a “not found” error message, but this is expected as we haven’t set up any routes. By default it returns JSON data, but we can choose to accept edn instead by adding in an Accept header:

$ curl -H Accept:application/edn http://localhost:3000/
{:error :not-found}

Let’s take a look at the config.edn file for this project:

{:duct.core/project-ns  foo
 :duct.core/environment :production

 :duct.module/logging {}
 :duct.module.web/api {}
 :duct.module/sql {}

 :duct.module/ataraxy
 {}}

Instead of the :duct.module/web module, we’re using the more specific :duct.module.web/api. That gives us content negotiation with JSON and edn.

There’s also a :duct.module/ataraxy module. That’s where we’re going to add some routes, but before we do that, let’s attend to the database first and add a couple of new keys to our configuration:

:duct.migrator/ragtime
{:migrations [#ig/ref :foo.migration/create-things]}

[:duct.migrator.ragtime/sql :foo.migration/create-things]
{:up   ["CREATE TABLE things (id INTEGER PRIMARY KEY, name TEXT)"]
 :down ["DROP TABLE things"]}

This create a migration that generates a things table. To run this migration, we just reset:

dev=> (reset)
:reloading (foo.main dev user)
:duct.migrator.ragtime/applying :foo.migration/create-things#d811dff6
:resumed

Our migration is loaded, and our things table is now in our development database.

One important feature of migrations is that until they’re committed to the production database, we can edit them freely. Say we wanted to add a quantity column. We could just change the migration accordingly:

[:duct.migrator.ragtime/sql :foo.migration/create-things]
{:up   ["CREATE TABLE things (id INTEGER PRIMARY KEY, name TEXT, quantity INTEGER)"]
 :down ["DROP TABLE things"]}

When we run reset, Duct will roll back the older version of the migration, then apply the newer version:

dev=> (reset)
:reloading ()
:duct.migrator.ragtime/rolling-back :foo.migration/create-things#d811dff6
:duct.migrator.ragtime/applying :foo.migration/create-things#ae0427b0
:resumed

Next, let’s add some basic routes to access and update the data in this table:

:duct.module/ataraxy
{"/things"
 {[:post {thing :body-params}] [:thing/create thing]
  [:get]                       [:thing/list]
  [:get "/" id]                [:thing/fetch ^int id]}}

This sets up the routes. Notice that each key in the map denotes a route to be matched, and each value denotes a result. The result collects data matched from the route, and in some cases transforms it. For example, the id matched in the route is coerced into an integer.

Now we have the routes, we need to add keys for the corresponding handler functions:

:foo.handler.thing/create {:db #ig/ref :duct.database/sql}
:foo.handler.thing/list   {:db #ig/ref :duct.database/sql}
:foo.handler.thing/fetch  {:db #ig/ref :duct.database/sql}

All of these handlers depend on the database, but in other cases we might have routes that depend on different resources, such as caches, queues, or mailers.

The Ataraxy module is smart enough to know that the routing result :thing/* should be connected to the handler function created by the :foo.handler.thing/* key.

So far we’ve just been writing configuration, but now it’s time to write some actual code. We start by creating a foo.handler.thing namespace and adding some essential requires:

(ns foo.handler.thing
  (:require [ataraxy.response :as response]
            [clojure.java.jdbc :as jdbc]
            [duct.database.sql]
            [integrant.core :as ig]))

The next step is to define our boundary protocol. We could put this in a separate namespace (like foo.boundary.thing), but because this boundary is used by only one handler, we instead place it in the handler namespace:

(defprotocol Things
  (create-thing [db thing])
  (list-things  [db])
  (fetch-thing  [db id]))

We can implement these functions in various ways. Clojure has a variety of different SQL libraries, such as HoneySQL for those who like to write SQL in Clojure, and HugSQL for those who like to write SQL in SQL.

In this example, we’ll just use the no-frills clojure.java.jdbc library:

(extend-protocol Things
  duct.database.sql.Boundary
  (create-thing [{db :spec} thing]
    (val (ffirst (jdbc/insert! db :things thing))))
  (list-things [{db :spec}]
    (jdbc/query db ["SELECT * FROM things"]))
  (fetch-thing [{db :spec} id]
    (first (jdbc/query db ["SELECT * FROM things WHERE id = ?" id]))))

The boundary defines the grey area between where our application logic ends and the database logic begins. We can also mock the boundary in tests, so that our handler tests can be isolated from the database tests.

Speaking of handlers, it’s time we implement them:

(defmethod ig/init-key ::create [_ {:keys [db]}]
  (fn [{[_ thing] :ataraxy/result}]
    (let [id (create-thing db thing)]
      [::response/created (str "/things/" id)])))

(defmethod ig/init-key ::list [_ {:keys [db]}]
  (fn [_] [::response/ok (list-things db)]))
  
(defmethod ig/init-key ::fetch [_ {:keys [db]}]
  (fn [{[_ id] :ataraxy/result}]
    (if-let [thing (fetch-thing db id)]
      [::response/ok thing]
      [::response/not-found {:error :not-found}])))

Ataraxy stores the result of the route match in the :ataraxy/result key on the request map. The URL “/things/10”, for example, would have the result [::fetch 10]. We can pull the result out of the request map with some simple destructuring.

It’s important to note that this destructuring serves to filter out unnecessary information. We know that only the :ataraxy/result key matters, and when testing we can omit every other part of the request map.

Similarly, Ataraxy allows variants (vectors that contain a key/value pair) to be returned instead of response maps. This makes the return value easier to check in the REPL and to write tests around.

Let’s test this works. We first create a new resource:

$ curl -X POST -H Content-Type:application/json \
   -d '{"name": "Book", "quantity": 10}' \
   http://localhost:3000/things

Then by check the created resource is in the list:

$ curl http://localhost:3000/things
[{"id":1,"name":"Book","quantity":10}]

And the resource has it’s own URI:

$ curl http://localhost:3000/things/1
{"id":1,"name":"Book","quantity":10}

Looks like everything works as expected.

As Duct gains more modules, we can expect the process of creating an API to become more automated. Because Duct is a framework based around a data structure, we can extensively automate common operations, without losing the ability to customize when necessary.

As you build an API with Duct, keep modules in mind. If you find yourself writing very similar sets of handlers and routes, there may be a simple way of factoring out the code into a module.

For more about modules, read the duct/core documentation.