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:
- POST /books
- GET /books
- GET /books/:id
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-apiNow 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 runYou 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 replStart 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