REST APIs in Clojure 101

Kalpani Ranasinghe
8 min readJul 22, 2023
Photo by AltumCode on Unsplash

Clojure is a function programming language and it is a dialect of LISP. Features such as simplicity, immutability, concurrency, runtime polymorphism, and JVM support are the reasons behind the increased popularity of this programming language.

Rich Hickey developed Clojure in 2007, because he wanted a modern Lisp for functional programming, symbiotic with the established Java platform, and designed for concurrency — Wikipedia

I recently started learning Clojure and I can say it is not the easiest language to learn, if you always have used the object-oriented concepts when solving programming problems. For example, it’s similar to start thinking in a different language other than your mother tongue. But at the same time, it is more interesting to look at a problem in a different perspective.

In this article, I’m going to explain how to create REST APIs using Clojure. If you are a beginner or you don’t have any experience at all, I would like to suggest reading through the basic concepts and solving some small programming problems in platforms like Exercism first. That helped me to grasp the core basic concepts within a small time frame.

If you’re comfortable enough with the basics, this guide is for you!

Let’s get started…

First things first.

Before start coding, you need to set up the environment by installing Java SDK and Leiningen. After that, navigate to your working directory and give the following command to create a simple Clojure app with all the components that you need to get your app up and running.

lein new app rest-api

Then navigate into your newly created project and run it using the following commands.

cd rest-api
lein run

This will print “Hello, World!” in the command line.

If you open the src/core.clj file in an editor (preferably vscode) you can find the relevant code segment. To understand better, you can modify the code and see how the output changes accordingly.

Next, let’s see what dependencies are needed for this project.

  1. Ring — Ring is a Clojure web application library and it abstracts the details of HTTP into a simple, unified API. The ring library was inspired by Python’s WSGI and Ruby’s Rack.
  2. Compojure — Compojure is a routing library built on top of Ring.
  3. HTTP Kit — A concurrent, event-driven HTTP client/server for Clojure which is Ring compatible.
  4. Data.json — JSON library for Clojure.

To add these dependencies to your project, go to your project.clj file and replace the dependencies section using the following block of code lines.

:dependencies [[org.clojure/clojure "1.11.1"]
[compojure "1.7.0"]
; Our Http library for client/server
[http-kit "2.3.0"]
; Ring defaults - for query params etc
[ring/ring-defaults "0.3.4"]
; Clojure data.JSON library
[org.clojure/data.json "2.4.0"]]

The version of each library may have changed when you read this article. So keep in mind to change them into the most recent versions. If you run the application again, you can see in the terminal, the dependencies get installed into your project.

Let’s navigate into the core.clj file again. To use the dependencies we just installed, you need to add the following code snippet to this file. What it does is, it imports the installed dependencies into your code by using the :require clause in Clojure.

(ns rest-api.core
(:require [org.httpkit.server :as server]
[compojure.route :as route]
[clojure.string :as str]
[clojure.data.json :as json]
[compojure.core :refer [defroutes GET POST PUT DELETE]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]])
(:gen-class))

Okay, now we can move into the real implementation of the REST APIs.

In this application, I’m going to create 4 basic endpoints. They are as follows:

Now we can define these routes in our code as follows:

(defroutes app-routes
(GET "/users" [] user-handler)
(POST "/users/add" [] adduser-handler)
(PUT "/user/update" [] update-city)
(DELETE "/user/delete" [] delete-user))

Also, if someone tries to access a route that has not been defined, we can tell them the route is not defined by adding the following line to the route definition:

(route/not-found "Page not found!")

To simply explain routes in the route definition, I will take one route which is the easiest one.

(GET "/users" [] user-handler)

(GET …) is a macro defined by the Ring library. We can use it to define a route for handling HTTP GET requests. Likewise, we also have POST, PUT, and DELETE macros.

The first argument in this macro is a string representing the path of the route, which is, “/users” in this case. The vector of middleware is the second argument, but in this example, we can keep it empty ([]) since we don’t need to add any middleware for this example. Mostly we use them to modify the parameters that we are sending through a request.

As the third argument, we have a handler function name which in this case is “user-handler”. This function will be called upon when the HTTP GET request matches the defined route (“/users”). Upon request sending, this function will send a JSON response containing the user details.

So, I think now you can easily understand how the other three routes are working.

Before creating the event-handlers we need to define a way to store user details. Since this is a simple example, we can use a collection inside the current code. We use the atom datatype for this and it serves as a mutable container to store a collection of user data. The atom can be updated using functions like swap! and reset!. For example, to add a user to the collection, you can use swap! to modify the atom’s value.

(def users-collection (atom []))
(def id-counter (atom 0))

Also, we need a separate variable as an id counter to track the last id, when adding a new user. Now, we can store some users in our users- collection using the adduser function given below.

(defn adduser [firstname surname city]
(swap! id-counter inc)
(let [user {:id @id-counter
:firstname (str/capitalize firstname)
:surname (str/capitalize surname)
:city city}]
(swap! users-collection conj user)
user))

; Example JSON objects
(adduser "Jane" "Smith", "Oulu")
(adduser "John" "Doe", "Helsinki")

Then we need to implement four main event handlers. The user-handler retrieves the data in users-collection as a JSON string. adduser-handler adds a new user to the users-collection JSON string. In update-city handler, it updates the city of the given user ID while delete-user handler deletes the user of the given ID. The relevant codes are given in the following section.

; Return List of Users
(defn user-handler [req]
{:status 200
:headers {"Content-Type" "text/json"}
:body (str (json/write-str @users-collection))})

; Add a new person into the users-collection
(defn adduser-handler [req]
{:status 200
:headers {"Content-Type" "text/json"}
:body (-> (let [firstname (-> req :params :firstname)
surname (-> req :params :surname)
city (-> req :params :city)]
(adduser firstname surname city)
(str (json/write-str @users-collection))))})

; Updates the city of the given user id
(defn update-city [req]
(let [id (-> req :params :id)
city (-> req :params :city)]
(swap! users-collection
(fn [users]
(mapv (fn [user]
(if (= (:id user) (Integer. id))
(assoc user :city city)
user))
users)))
{:status 200
:headers {"Content-Type" "text/json"}
:body (json/write-str @users-collection)}))

; Deleted the user of the given id
(defn delete-user [req]
(let [id (-> req :params :id)]
(swap! users-collection
(fn [users]
(->> users
(filter #(not= (:id %) (Integer. id)))
vec)))
{:status 200
:headers {"Content-Type" "text/json"}
:body (json/write-str @users-collection)}))

As you can see, handler functions return maps.

:status defines the status code of the HTTP response, and :headers represent the response headers such as authentication headers, content type, etc. :body contains the actual body of the response and it is the updated JSON string of users-collection for all the handler events.

Ring middleware processes these maps to generate appropriate HTTP responses and sends them back to the client.

Last but not least, we need to update the main function in the core.clj.

(defn -main
[& args]
(let [port (Integer/parseInt (or (System/getenv "PORT") "4000"))]
(server/run-server
(-> app-routes
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false)))
{:port port})
(println (str "Webserver started at http:/127.0.0.1:" port "/"))))

In this main function, we have first defined the port in which we are running the application and then added the codes that are needed to run the server with ring defaults.

Now you can run lein run in the command line. If everything works as expected it will print out “Webserver started at http:/127.0.0.1:4000” in the console.

lein run

To test the endpoints, we can use Postman. For example, you can first check adding-a-user functionality as follows:

POST request

Here you can see the parameters that I have sent, have been added as a user in the JSON response.

Also, we can modify the city name of one of the users, and we can delete a user in a separate request. The following images showcase how these requests can be sent using Postman.

PUT request to modify city value
Deleting a user

As the last remark, you can always retrieve the users list using the GET request http://127.0.0.1:4000/users.

Retrieving the users using the GET request

Good job! Now you know how to create a simple set of APIs in Clojure.

Photo by Delaney Dawson on Unsplash

Also, you can find the completed code in this repository which you can clone right away. I hope this small guide will help someone who is getting started with Clojure.

If you have any suggestions, or constructive feedbacks please feel free to mention them in the comments section below.

Happy coding! Thank you for reading. :)

References

clojure/data.json: JSON in Clojure (github.com)

▷ Clojure Tutorial | A Complete Guide on Introduction to Clojure (mindmajix.com)

Clojure — Atoms | Tutorialspoint

weavejester/compojure: A concise routing library for Ring/Clojure (github.com)

Writing Clojure Webapps with Ring | Baeldung

My First Clojure Backend Using Ring, Jetty and Compojure | Otee’s Notes on Programming

Building a REST API in Clojure. HttpKit, Compojure and data.JSON make… | by Functional Human | The Startup | Medium

Clojure — Wikipedia

Quick Clojure Effective Functional Programming — Mark McDonnell

--

--

Kalpani Ranasinghe

Backend Developer | Graduate Student at University of Oulu