REST APIs with Clojure — Part I — Working with the APIs

Welcome to the first of a 2-part series covering various aspects on working with REST APIs in Clojure.

Through the series, I intend to cover the following topics :

With that clarified and without further ado, let’s get started.

Prerequisites:

Setup the Clojure Project Management Tool

Leiningen is the de-facto tool for working with Clojure projects. It focuses on project automation and declarative configurations making it easy for developers to focus on the code rather on the nitty gritties of setting things up like dependencies etc. Let us get it installed.

Install Leiningen

You can follow the instructions here — https://leiningen.org/#install

On MAC run this :

> brew install lein

On other platforms this should work :

> bash -c “cd ~ && curl -O https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein && chmod +x lein”> cd /usr/local/bin> ./lein

This should download the self-installing package and install it for you.

With lein installed, let’s try it out.

Fire up the REPL :

Open up a terminal and run :

> lein repl

It might ask you if you want to run it in a context without a project, type in ‘y’ and go ahead. You should see something like this.

nREPL server started on port 56657 on host 127.0.0.1 — nrepl://127.0.0.1:56657REPL-y 0.5.1, nREPL 0.8.3Clojure 1.10.3Java HotSpot(TM) 64-Bit Server VM 16.0.2+7–67Docs: (doc function-name-here)(find-doc “part-of-name-here”)Source: (source function-name-here)Javadoc: (javadoc java-object-or-class-here)Exit: Control+D or (exit) or (quit)Results: Stored in vars *1, *2, *3, an exception in *euser=>

Let’s try out a few things here :

At the REPL prompt, type what’s below and press Return

(* 2 3)

This should be evaluated to 6

6

Type the below and press Return

(println “Hello World”)

You should see this result

Hello Worldnil

Try the below like before on the REPL.

(clojure.string/upper-case “what a wonderful day!”)

You will see a result like below :

“WHAT A BEAUTIFUL DAY!”

Now that we have quickly verified that the basic things are working fine with Clojure, let us move ahead and create the project that we will be using for the rest of the article.

Create a Project

We will use the amazing lein tool for this

On a terminal, head to a directory where you want the Clojure project to be created and execute the below command

lein new app rest-api-tutorial

you should see an output like so:

Generating a project called rest-api-tutorial based on the ‘app’ template.

Change into the project directory

cd rest-api-tutorial

and you should have a folder structure created like below:

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│ └── intro.md
├── project.clj
├── resources
├── src
│ └── rest_api_tutorial
│ └── core.clj
├── target
│ └── default
│ ├── classes
│ │ └── META-INF
│ │ └── maven
│ │ └── rest-api-tutorial
│ │ └── rest-api-tutorial
│ │ └── pom.properties
│ ├── repl-port
│ └── stale
│ └── leiningen.core.classpath.extract-native-dependencies
└── test
└── rest_api_tutorial
└── core_test.clj

Have highlighted the core files that we will be working with.

Now, Execute

lein run

You should see this on the terminal

Hello, World!

As you see, lein has created the boiler plate needed for a quick ‘Hello World’ application in Clojure. Thats cool!

We have a running application. Let us now get to understanding how to work with REST APIs in Clojure.

Before diving into the Clojure libraries, let us understand the REST API that we will be working with.

The REST API end points that I will be using in this instalment of the tutorial would be The One API — https://the-one-api.dev/

This set of end points exposes a variety of data around the J.R.R. Tolkien’s masterpiece, The Lord of the Rings series — the books, the chapters, the movies, the characters, some interesting quotes and so on. For all the non-LOTR fans out there, please bear with me :-)

Before jumping into the Coljure REPL, let us quickly try out a few of the end points on a terminal.

Trying out the REST APIs :

Open up a terminal and type the below ( ensure you have ‘curl’ present in your path )

curl -X GET https://the-one-api.dev/v2/book

you should see something like this printed out :

{"docs":[{"_id":"5cf5805fb53e011a64671582","name":"The Fellowship Of The Ring"},{"_id":"5cf58077b53e011a64671583","name":"The Two Towers"},{"_id":"5cf58080b53e011a64671584","name":"The Return Of The King"}],"total":3,"limit":1000,"offset":0,"page":1,"pages":1}

It returns some details about the 3 books in the series.

Let us try a few more examples :

Get details of a specific book ( with the book id which is the _id field from the previous response) :

curl -X GET https://the-one-api.dev/v2/book/5cf5805fb53e011a64671582

returns this :

{“docs”:[{“_id”:”5cf5805fb53e011a64671582",”name”:”The Fellowship Of The Ring”}],”total”:1,”limit”:1000,”offset”:0,”page”:1,”pages”:1}

Get the chapter listings of a particular book

curl -X GET https://the-one-api.dev/v2/book/5cf5805fb53e011a64671582/chapter

returns the response like below :

{"docs":[{"_id":"6091b6d6d58360f988133b8b","chapterName":"A Long-expected Party"},{"_id":"6091b6d6d58360f988133b8c","chapterName":"The Shadow of the Past"},{"_id":"6091b6d6d58360f988133b8d","chapterName":"Three is Company"},{"_id":"6091b6d6d58360f988133b8e","chapterName":"A Short Cut to Mushrooms"},{"_id":"6091b6d6d58360f988133b8f","chapterName":"A Conspiracy Unmasked"},{"_id":"6091b6d6d58360f988133b90","chapterName":"The Old Forest"},{"_id":"6091b6d6d58360f988133b91","chapterName":"In the House of Tom Bombadil"},{"_id":"6091b6d6d58360f988133b92","chapterName":"Fog on the Barrow-Downs"},{"_id":"6091b6d6d58360f988133b93","chapterName":"At the Sign of The Prancing Pony"},{"_id":"6091b6d6d58360f988133b94","chapterName":"Strider"},{"_id":"6091b6d6d58360f988133b95","chapterName":"A Knife in the Dark"},{"_id":"6091b6d6d58360f988133b96","chapterName":"Flight to the Ford"},{"_id":"6091b6d6d58360f988133b97","chapterName":"Many Meetings"},{"_id":"6091b6d6d58360f988133b98","chapterName":"The Council of Elrond"},{"_id":"6091b6d6d58360f988133b99","chapterName":"The Ring Goes South"},{"_id":"6091b6d6d58360f988133b9a","chapterName":"A Journey in the Dark"},{"_id":"6091b6d6d58360f988133b9b","chapterName":"The Bridge of Khazad-dûm"},{"_id":"6091b6d6d58360f988133b9c","chapterName":"Lothlórien"},{"_id":"6091b6d6d58360f988133b9d","chapterName":"The Mirror of Galadriel"},{"_id":"6091b6d6d58360f988133b9e","chapterName":"Farewell to Lórien"},{"_id":"6091b6d6d58360f988133b9f","chapterName":"The Great River"},{"_id":"6091b6d6d58360f988133ba0","chapterName":"The Breaking of the Fellowship"}],"total":22,"limit":1000,"offset":0,"page":1,"pages":1}

Now that we have verified that the REST endpoints are ready and working, let us dive into accessing them from Clojure and try to play around with the results.

Configure the Project Dependencies :

For interacting with REST APIs in Clojure, we would need to add couple of libraries as dependencies to our project.

Open up the project.clj file in an editor (you should find the file under the rest-api-tutorial folder that we created before)

Update the :dependencies section in the file with the one shown below

:dependencies [[org.clojure/clojure "1.10.3"]
[clj-http "3.12.3"]
[cheshire "5.10.0"]]

Your final project.clj file would look like below :

(defproject rest-api-tutorial "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.3"]
[clj-http "3.12.3"]
[cheshire "5.10.0"]]

:main ^:skip-aot rest-api-tutorial.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

Here, we are adding two dependencies.

a. clj-http — is a http client library, a Clojure wrapper on the Java’s Apache HttpComponents client.

b. cheshire — a popular library for working JSON data

With the projects.clj file updated, lein should automatically download your dependencies. Just to ensure that it does, you can also run this :

lein deps

This should download and setup all the dependencies that we have mentioned in the project.clj file

Ok, with the REST API end points working and our project configured with all the required dependencies, let us now jump into invoking the API end points from Clojure.

Invoking the REST APIs from Clojure

Head to the project directory that we created

cd rest-api-tutorial

Then, fire up the REPL

lein repl

Firstly, we will have to do a require of the dependencies, the libraries that we have configured for the project.

At the REPL prompt, type in

(require ‘[clj-http.client :as client])

Here, we are importing the clj-http.client namespace and giving it an alias of client

We now make a request to the book endpoint. Type the below in the REPL and press Enter :

(client/get “https://the-one-api.dev/v2/book")

You see the HTTP response

Let us store the response in a variable and play with it :

(def resp (client/get “https://the-one-api.dev/v2/book"))

This associates the symbol resp with the data returned by the HTTP request.

Let us check what type the resp var is

(class resp)

Indicates that it is clojure.lang.PersistentHashMap

Let us gather some keys from the map like so:

(keys resp)(:status resp)(:headers resp)(:body resp)

We are interested in the body value. Let us capture that in a different var

(def body (:body resp))

Let us check the type of ‘body’

(class body)

It indicates that it is a String. Now, that’s interesting! It is a JSON structure captured as a String. Would be good to be able to parse it back into a JSON data structure right. Let us do that.

For that we need to require the second library that we added as a dependency.

(require ‘[cheshire.core :as json])

Same as before, we are importing the cheshire.core namespace and giving it an alias of json

Then, let us use a method from the library to parse the string output into a JSON

(def body-parsed (json/parse-string body))(class body-parsed)

Now, the parsed body value indicates that it is a clojure.lang.PersistentArrayMap

(keys body-parsed);; (“docs” “total” “limit” “offset” “page” “pages”)

However, if we try to access the individual key’s value as before like this:

(:docs body-parsed)nil

It doesn’t work. Because, the keys are not symbols, they are strings. To get their values, you need to query a bit differently like so:

(get body-parsed “docs”);; [{“_id” “5cf5805fb53e011a64671582”, “name” “The Fellowship Of The Ring”} {“_id” “5cf58077b53e011a64671583”, “name” “The Two Towers”} {“_id” “5cf58080b53e011a64671584”, “name” “The Return Of The King”}]

To make things more standardised and simpler, let us convert the keys to symbols. This needs to be done at the time of parsing of the response body by passing a value of true to the parse-string method like so :

(def body-parsed (json/parse-string body true))(body-parsed)(keys body-parsed);; (:docs :total :limit :offset :page :pages)

Now, you see that the keys are symbols. Let us fetch the value associated with the ‘docs’ key

(:docs body-parsed);; [{:_id “5cf5805fb53e011a64671582”, :name “The Fellowship Of The Ring”} {:_id “5cf58077b53e011a64671583”, :name “The Two Towers”} {:_id “5cf58080b53e011a64671584”, :name “The Return Of The King”}]

The result as we see is a vector of maps. Each map is a book with its id and name details.

To print out the names of the books, we can do something like below:

(map #(:name %) (:docs body-parsed));; (“The Fellowship Of The Ring” “The Two Towers” “The Return Of The King”)

Here, we map an anonymous function over the vector of maps that we got earlier. The anonymous function as you can see picks out the value associated with the :name key from each element of the vector which is a map.

With the above we have seen how to send a GET request to a REST end point, fetch the data and process it. In this case though, the API end point didn’t need any authorization. However, that is not usually the case.

With The One API, other than a few end points which are open, the rest need authorization in the form of Bearer tokens.

To get a token, you need to signup here — https://the-one-api.dev/sign-up . Once you have the token, save it some place as you will be needing to work with rest of the endpoints.

With Authorization :

Let us try some of the REST endpoints that need authorization. Jump back into the REPL, type :

(def resp (client/get “https://the-one-api.dev/v2/movie" {:headers {:Authorization “Bearer <your_token_here>”}}))

Do replace <your_token_here> with your API token. Here, as you notice, we are passing the ‘Authorization’ token that is needed, as a part of the request header.

The result is something like this ( here, I am using pprint to do a pretty print of the JSON on to the terminal )

{:cached nil,:request-time 850,:repeatable? false,:protocol-version {:name “HTTP”, :major 1, :minor 1},:streaming? true,:http-client#object[org.apache.http.impl.client.InternalHttpClient 0x6f7dfcbc “org.apache.http.impl.client.InternalHttpClient@6f7dfcbc”],:chunked? false,
.....
.....
:body“{\”docs\”:[{\”_id\”:\”5cd95395de30eff6ebccde56\”,\”name\”:\”The Lord of the Rings Series\”,\”runtimeInMinutes\”:558,\”budgetInMillions\”:281,\”boxOfficeRevenueInMillions\”:2917,\”academyAwardNominations\”:30,\”academyAwardWins\”:17,\”rottenTomatoesScore\”:94},{\”_id\”:\”5cd95395de30eff6ebccde57\”,\”name\”:\”The Hobbit Series\”,\”runtimeInMinutes\”:462,\”budgetInMillions\”:675,\”boxOfficeRevenueInMillions\”:2932,\”academyAwardNominations\”:7,\”academyAwardWins\”:1,\”rottenTomatoesScore\”:66.33333333},{\”_id\”:\”5cd95395de30eff6ebccde58\”,\”name\”:\”The Unexpected Journey\”,\”runtimeInMinutes\”:169,\”budgetInMillions\”:200,\”boxOfficeRevenueInMillions\”:1021,\”academyAwardNominations\”:3,\”academyAwardWins\”:1,\”rottenTomatoesScore\”:64},{\”_id\”:\”5cd95395de30eff6ebccde59\”,\”name\”:\”The Desolation of Smaug\”,\”runtimeInMinutes\”:161,\”budgetInMillions\”:217,\”boxOfficeRevenueInMillions\”:958.4,\”academyAwardNominations\”:3,\”academyAwardWins\”:0,\”rottenTomatoesScore\”:75},{\”_id\”:\”5cd95395de30eff6ebccde5a\”,\”name\”:\”The Battle of the Five Armies\”,\”runtimeInMinutes\”:144,\”budgetInMillions\”:250,\”boxOfficeRevenueInMillions\”:956,\”academyAwardNominations\”:1,\”academyAwardWins\”:0,\”rottenTomatoesScore\”:60},{\”_id\”:\”5cd95395de30eff6ebccde5b\”,\”name\”:\”The Two Towers \”,\”runtimeInMinutes\”:179,\”budgetInMillions\”:94,\”boxOfficeRevenueInMillions\”:926,\”academyAwardNominations\”:6,\”academyAwardWins\”:2,\”rottenTomatoesScore\”:96},{\”_id\”:\”5cd95395de30eff6ebccde5c\”,\”name\”:\”The Fellowship of the Ring\”,\”runtimeInMinutes\”:178,\”budgetInMillions\”:93,\”boxOfficeRevenueInMillions\”:871.5,\”academyAwardNominations\”:13,\”academyAwardWins\”:4,\”rottenTomatoesScore\”:91},{\”_id\”:\”5cd95395de30eff6ebccde5d\”,\”name\”:\”The Return of the King\”,\”runtimeInMinutes\”:201,\”budgetInMillions\”:94,\”boxOfficeRevenueInMillions\”:1120,\”academyAwardNominations\”:11,\”academyAwardWins\”:11,\”rottenTomatoesScore\”:95}],\”total\”:8,\”limit\”:1000,\”offset\”:0,\”page\”:1,\”pages\”:1}”,:trace-redirects []}

As always, the ‘body’ part is what we are interested in and that needs to be symbolized too. Type in

(def body-parsed (json/parse-string (:body resp) true))(pprint body-parsed)

This the result you see :

{:docs[{:_id “5cd95395de30eff6ebccde56”,:name “The Lord of the Rings Series”,:runtimeInMinutes 558,:budgetInMillions 281,:boxOfficeRevenueInMillions 2917,:academyAwardNominations 30,:academyAwardWins 17,:rottenTomatoesScore 94}
.......
.......
{:_id “5cd95395de30eff6ebccde5d”,:name “The Return of the King”,:runtimeInMinutes 201,:budgetInMillions 94,:boxOfficeRevenueInMillions 1120,:academyAwardNominations 11,:academyAwardWins 11,:rottenTomatoesScore 95}],:total 8,:limit 1000,:offset 0,:page 1,:pages 1}

Now, let us process this to get the names of the movies

(map #(:name %) (:docs body-parsed))

This lists the series’ names as well. We are interested only in the movies. Let us discard the series names like so:

(drop 2 (map #(:name %) (:docs body-parsed)))

and let us pretty print it too

(pprint (drop 2 (map #(:name %) (:docs body-parsed))));; (“The Unexpected Journey”“The Desolation of Smaug”“The Battle of the Five Armies”“The Two Towers ““The Fellowship of the Ring”“The Return of the King”)

There we have it, the list of movies neatly processed.

As a final set of examples, let us gather some information regarding the various characters in the Lord of the Ring series.

(def resp (client/get “https://the-one-api.dev/v2/character?limit=50" {:headers {:Authorization “Bearer <your_token_here>”}}))

Again, do not forget to replace <your_token_here> with your API token. Notice that we are limiting the result set to ’50’ characters as LOTR has thousands of characters by passing in a query param. And as before, let us parse the body by symbolizing the results.

(def body-parsed (json/parse-string (:body resp) true))

Some part of the parsed result is as below :

{:docs
[{:_id “5cd99d4bde30eff6ebccfbbe”,
:race “Human”,
:name “Adanel”,
:realm “”,
:spouse “Belemir”,
:hair “”,
:gender “Female”,
:death “”,
:wikiUrl “http://lotr.wikia.com//wiki/Adanel",
:birth “”,
:height “”}
{:_id “5cd99d4bde30eff6ebccfbbf”,
:race “Human”,
:name “Adrahil I”,
:realm “”,
:spouse “”,
:hair “”,
:gender “Male”,
:death “Late ,Third Age”,
:wikiUrl “http://lotr.wikia.com//wiki/Adrahil_I",
:birth “Before ,TA 1944”,
:height “”}
......
......
{:_id "5cd99d4bde30eff6ebccfbc6",
:race "Human",
:name "Almiel",
:realm "",
:spouse "",
:hair "",
:gender "Female",
:death "Early ,Second Age",
:wikiUrl "http://lotr.wikia.com//wiki/Almiel",
:birth "Between ,SA 700, and ,SA 750",
:height ""}]
}

The result has a bunch of characters spread across two races ‘Humans’ and ‘Elves’. Let us list out only the ‘Elves’ like so:

(def elves (filter #(= (:race %) “Elf”) (:docs body-parsed )))

and pretty print that too:

({:_id "5cd99d4bde30eff6ebccfbc1",
:race "Elf",
:name "Aegnor",
:realm "",
:spouse "Loved ,Andreth but remained unmarried",
:hair "Golden",
:gender "Male",
:death "FA 455",
:wikiUrl "http://lotr.wikia.com//wiki/Aegnor",
:birth "YT during the ,Noontide of Valinor",
:height ""}
{:_id "5cd99d4bde30eff6ebccfbca",
:race "Elf",
:name "Amarië",
:realm "",
:spouse "Loved ,Finrod, but it is unknown whether they married",
:hair "",
:gender "Female",
:death "Still alive",
:wikiUrl "http://lotr.wikia.com//wiki/Amari%C3%AB",
:birth "YT",
:height ""}
......
......
{:_id "5cd99d4bde30eff6ebccfbdc",
:race "Elf",
:name "Angrod",
:realm "",
:spouse "Eldalótë",
:hair "Golden",
:gender "Male",
:death "FA 455",
:wikiUrl "http://lotr.wikia.com//wiki/Angrod",
:birth "YT",
:height ""}
{:_id "5cd99d4bde30eff6ebccfbee",
:race "Elf",
:name "Aranwë",
:realm "",
:spouse "Unnamed wife",
:hair "",
:gender "Male",
:death "",
:wikiUrl "http://lotr.wikia.com//wiki/Aranw%C3%AB",
:birth "",
:height ""})

Like before, let us list down the names and the count of the Elves that we have got :

(def elves (filter #(= (:race %) “Elf”) (:docs body-parsed )))(pprint (map #(:name %) elves))(count elves)

result is

("Aegnor"
"Amarië"
"Amroth"
"Anairë"
"Amras"
"Amdír"
"Amrod"
"Annael"
"Angrod"
"Aranwë")
10

It could go on, limited only by one’s imagination for gathering and processing the data :-)

Tying up all together :

Now, let us tie up all the individual pieces of Clojure code that we were executing into function definitions. The complete code is as below :

rest-api-tutorial/src/rest_api_tutorial/core.clj :

(ns rest-api-tutorial.core
(:gen-class)
(:require [clj-http.client :as client]
[cheshire.core :as json]))
(def book-url "https://the-one-api.dev/v2/book")
(def movie-url "https://the-one-api.dev/v2/movie")
(def characters-url "https://the-one-api.dev/v2/character?limit=50")
;; Generate the auth header map using the api token env variable
(def token-string (str "Bearer " (System/getenv "ONE_API_TOKEN")))
(def header-map (assoc-in {} [:headers :Authorization] token-string))
;; Get characters
(defn get-elf-characters
[url]
(let [resp (client/get url header-map)
body-parsed (json/parse-string (:body resp) true)]
(def elves (filter #(= (:race %) "Elf") (:docs body-parsed )))
;;(pprint elves)
(println (str "Number of Elves we got : " (count elves)))
(map #(:name %) elves)
))
;; Get movies
(defn get-movies
[url]
(let [resp (client/get url header-map)
body (json/parse-string (:body resp) true)]
(map #(:name %) (drop 2 (:docs body)))))
;; Get books
(defn get-books
[url]
(let [resp (client/get url)
body (json/parse-string (:body resp) true)]
(map #(:name %) (:docs body))))
(defn -main
[arg]
(case arg
"books" (println (str "The books are: \n" (clojure.string/join "\n" (get-books book-url)) ))
"movies" (println (str "The movies are: \n" (clojure.string/join "\n" (get-movies movie-url)) ))
"elves" (println (str "The Elves are: \n" (clojure.string/join "\n" (get-elf-characters characters-url)) ))
)
)

Replace the code in core.clj under the rest-api-tutorial/src/rest_api_tutorial/folder, with the above.

. This should be set to your One-API token

You can do that by running the command below ( on Mac and Unix based systems ):

export ONE_API_TOKEN=<your_api_token>

On Windows, the below should work :

set ONE_API_TOKEN=<your_api_token>

Then execute the application using the command below :

lein run books

You should see a response like below :

The books are:
The Fellowship Of The Ring
The Two Towers
The Return Of The King

books here, is the cmd line argument passed to the application

The other arguments that work as of now are movies & elves

lein run moviesThe movies are:
The Unexpected Journey
The Desolation of Smaug
The Battle of the Five Armies
The Two Towers
The Fellowship of the Ring
The Return of the King
lein run elvesNumber of Elves we got : 10
The Elves are:
Aegnor
Amarië
Amroth
Anairë
Amras
Amdír
Amrod
Annael
Angrod
Aranwë

Feel free to play around with the code.

That brings us to the end of Part-I. We have been able to successfully create and configure a Clojure project, define the dependencies, invoke the REST API end points and tie them all up into an application. Hope this gave you a flavour of how to work with REST APIs in Clojure and was helpful.

In the next part, we will be building our own REST CRUD APIs that interact with a PostgreSQL database. We will be building a small application that supports the use case.

Until then, happy Clojuring, if that is a word :-)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Santhosh Krishnamoorthy

Passionate Technologist. Also, a Naturalist and a Nature Photographer. Find my Wildlife & Nature Photography blog @ framesofnature.com