Getting started with compojure-api
Apr 28, 2016 · 4 minute read clojureIn this post, I will give a walkthrough of building a simple service using compojure-api. As a use case we will build a simple account service that offers two functions:
-
creating an account with some initial balance
-
transferring some amount from one account to another.
By the end of this tutorial you should have a swagger UI that allows testing these two calls:
The only prerequisite is that you install Leiningen. Leiningen provides build automation and dependency management for Clojure projects. If you’re on Mac OS and using brew
you can install it by opening a shell and running brew install leiningen
. If you’re on another platform follow the instructions at leiningen.org. After installing leiningen you should be able to run lein --version
:
$ lein --version
Leiningen 2.6.1 on Java 1.8.0_31 Java HotSpot(TM) 64-Bit Server VM
Create a project
With leiningen
installed run lein new account-service
:
$ lein new account-service
$ cd account-service
$ tree
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│ └── intro.md
├── project.clj
├── resources
├── src
│ └── account_service
│ └── core.clj
└── test
└── account_service
└── core_test.clj
Getting started with Ring
Before diving into compojure-api let’s demonstrate a simple use case using the ring library. Take a look inside src/account-service/core.clj
:
(ns account-service.core)
(defn foo
"I don't do a whole lot."
[x]
(println x "Hello, World!"))
Replace foo
with a simple handler
function:
(ns account-service.core)
(defn handler [request]
{:body "Hello from ring"})
We will use this handler
function to handle all incoming requests. As you can see it just returns a simple map
in which we have an entry with key :body
and a string
as the value. Now take a look inside project.clj
:
(defproject account-service "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]])
You can remove the :description
, :url
and :license
entries for now, and add the ring
dependency, the lein-ring
plugin and the reference to the ring handler
which we wrote in the previous step:
(defproject account-service "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"]
[ring "1.4.0"]]
:ring {:handler account-service.core/handler}
:profiles {:dev
{:plugins [[lein-ring "0.9.7"]]}})
That’s it. We have instructed Ring
where it can find the function to process all incoming HTTP
requests. Now let’s run our simple web application. Open a shell and run:
$ lein ring server-headless
...
Started server on port 3000
This starts a server which will handle incoming HTTP
requests on port 3000. In another shell we can test the server by running:
$ curl http://localhost:3000/
Hello from ring
As you can see we have the response containing Hello from ring
which we wrote previously in our handler
.
Live reload
With the ring server still running, change the message in the handler
function in core.clj
, save the file, and rerun curl http://localhost:3000/
. You should see the updated message.
Using compojure-api
The compojure-api library makes it very easy to build web APIs. It is built on top of compojure and ring. It also uses the schema library for describing and validating requests, and ring-swagger to expose API documentation. First, let’s add some requires in core.clj
:
(ns account-service.core
(:require [compojure.api.sweet :refer :all]
[ring.util.http-response :refer :all]
[schema.core :as s]
[ring.swagger.schema :as rs]))
Next, we define schemas for the payloads:
(s/defschema Account
{:id Long
:balance s/Num})
(s/defschema NewAccount (dissoc Account :id))
(s/defschema Transfer
{:id Long
:from-account s/Int
:to-account s/Int
:amount s/Num
:status s/Keyword})
(s/defschema NewTransfer (dissoc Transfer :id :status))
And finally let’s define our routes, one to create an account, and one to request a transfer. Replace the handler function we wrote earlier with:
(def app
(api
{:swagger
{:ui "/"
:spec "/swagger.json"
:data {:info {:title "Account Service"}
:tags [{:name "api"}]}}}
(context "/api" []
:tags ["api"]
(POST "/account" []
:body [account (describe NewAccount "new account")]
(ok))
(POST "/transfer" []
:body [transfer (describe NewTransfer "new transfer")]
(ok)))))
Finally replace ring
with the compojure-api
dependency in project.clj
:
(defproject account-service "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"]
[metosin/compojure-api "1.0.2"]]
:ring {:handler account-service.core/app}
:profiles {:dev
{:plugins [[lein-ring "0.9.7"]]
:dependencies [[javax.servlet/servlet-api "2.5"]]}})
With that in place run lein ring server
in the shell and go to http://localhost:3000/index.html in your browser. You should see a Swagger UI which allows you to try out the endpoints:
You should get 200
for valid requests and 400
for requests that don’t conform to the schema we defined.
Packaging
For packaging your app, you can either create an uberjar
and then simply run it like this:
$ lein ring uberjar
$ java -jar target/account-service-0.1.0-SNAPSHOT-standalone.jar
or create a war
and deploy it in your favorite container:
$ lein ring uberwar
Conclusion
In this post, we have exposed a simple API using compojure-api. You can find all the source code on GitHub. In the next post, we will show how to use this in conjunction with Datomic for persistence.