Simple Book REST API on Clojure

After my post here in regard to clojure I keep exploring with it. I have been reading a book in regard to micro services using clojure, and got in touch with Pedestal framework.

Here I am going to use Pedestal to build a micro service to serve a simple REST API for books, using JSON, with basic operations:

Pedestal Project #

We are able to use leiningen to create a new project with Pedestal configured, out of the box. There is a template for pedestal pedestal-service. We use it this way:

lein new pedestal-service microservice-api

Now we have a project configured to use pedestal. A created microservice-api folder was created.

We can start the server just doing at microservice-api folder:

lein run

You can now go to your http://localhost:8080 and see an Hello Word! message.

To understand how this works it is important to analyse the entry file src/microservice_api/server.clj and src/microservice_api/service.clj where our routes are defined.

It is interesting to see ring library getting around again.

A sad news is this setup does not do the auto reload of the code when we change it. I am used to auto reload. So, I started digging in how to do it and got the idea we should use REPL in dev mode:

Start repl

lein repl

Start the server

(def serv (run-dev))

When we change the code we need to execute:

(use 'microservice-api.service :reload)

I do not like it very much. I want to look for a better approach, since I am used to auto reloads, and I think it improves our productivity.

We can see more about it here and here

Routes #

Looking to code on src/microservice_api/service.clj we can get an idea how a route could be defined.

(defn about-page
  [request]
  (ring-resp/response (format "Clojure %s - served from %s"
                              (clojure-version)
                              (route/url-for ::about-page))))

;; Defines "/" and "/about" routes with their associated :get handlers.
;; The interceptors defined after the verb map (e.g., {:get home-page}
;; apply to / and its children (/about).
(def common-interceptors [(body-params/body-params) http/html-body])

;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]})

This is the code creating a route for /about. A function about-page is defined for this route, returning a response with a formatted string which contains the clojure version and the full url for the about page route.

Then we have the routes vector where we define the /aboutto be intercepted on http GET method and use about-page function.

Simple Books API #

Lets create the routes for our books API. I am here storing the books in memory. Off course it is not a good approach. Next I want to start using a database, maybe datomic, created by clojure team as well, or a more traditional relational database like postgresql.

After a bit of playing with clojure and Pedestal framework I ended adding the following code to src/microservice_api/service.clj:

;; do not do it -- just for experimenting with data in memory while learning
(def books [])
(def current-id 0)

(defn create-book
  [request]
  (def current-id (+ current-id 1))
  (def new-book (assoc (:json-params request) :id current-id))
  (def books (conj books new-book))
  (ring-resp/response (format "{ id: %d }" current-id)))

(defn all-books
  [request]
  (ring-resp/response (cheshire.core/generate-string (assoc {} :books books))))

(defn find-book
  [request]
  (defn is-book? [book] (= (str (:id book)) (:id (:path-params request))))
  (def book (first (filter is-book? books)))
  (ring-resp/response (cheshire.core/generate-string book)))

;; Defines "/" and "/about" routes with their associated :get handlers.
;; The interceptors defined after the verb map (e.g., {:get home-page}
;; apply to / and its children (/about).
(def common-interceptors [(body-params/body-params) http/html-body])

;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/books" :post (conj common-interceptors `create-book)]
              ["/books" :get (conj common-interceptors `all-books)]
              ["/books/:id" :get (conj common-interceptors `find-book)]
              ["/about" :get (conj common-interceptors `about-page)]})

Some points here:
I am using cheshire.core/generate-stringto convert a map to json string and using (:json-params request) to access json payload request.

Published