Liberator + Clojure REST API, First approach - Part 2

In the first part of this approach, we started a clojure project with lein compojure template and we made some modifications on project.cl file.

In this post, I’ll explain every part of our project with more details.

Models

In this folder, we have a namespace called db. This namespace contains the basic database configuration, some utility functions and the basic database operations to be used with our entities.

Utility functions
;; Utility operations

(defn get-environment-var
  "Get the value of an environment variable"
  [name]
  (System/getenv name))

(defn convert-dates
  "Convert dates from miliseconds to java.sql.Timestamp object"
  [date1 date2]
  (hash-map
   :starting_date (java.sql.Timestamp. date1)
   :ending_date (java.sql.Timestamp. date2)))

(defn process-promotion
  "Merge a promotion map with new dates from convert-dates function"
  [promotion]
  (merge promotion
    (convert-dates (:starting_date promotion) (:ending_date promotion))))

As you can see, this code snippet contains 3 functions: get-environment-var, convert-dates and process-promotion.

get-environment-var function is used to get the value of an environment variable with a given name(I know this is just one single line, but the function name is easy to remember).

convert-dates converts two dates in milliseconds [date1 date2] into a map with the following structure:

{:starting_date java.sql.Timestamp. date1
 :ending_date java.sql.Timestamp. date2}

process-promotion merges our promotion value taken from the request with the map generated by convert-dates. This function simplifies the interaction with jdbc.

Basic database configuration data
;; Database configuration data

(def db {:subprotocol "postgresql"
         :subname (get-environment-var "CLOJUCHIPS_DB_URL")
         :user (get-environment-var "CLOJUCHIPS_DB_USER")
         :password (get-environment-var "CLOJUCHIPS_DB_PASS")})

Here we defined a value named db with the basic information about the database. Note that we are not exposing via hard-code our database information, instead we read our database parameters from the environment variables created in the previous post.

Generic operations

We have defined our basic operations for our entities: get all entities, get, update, create and delete an entity. So, for DRY principle we have these five generic operations in our db.clj too.

;; Generic operations

(defn read-all-items
  "Read all rows from any given entity"
  [table]
  (sql/query db [(str "select * from " table)]))

(defn read-one-item
  "Read an entity with an id provided by the client"
  [table id]
  (first (sql/query db [(str "select * from " table " where id=" id)])))

(defn create-one-item
  "Create an entity using json format"
  [table item]
  (if (= table "promotion")
   (sql/insert! db (keyword table) (process-promotion (parse-string (str item) true)))
   (sql/insert! db (keyword table) (parse-string (str item) true))))

(defn update-item
  "Update an item with an id provided by the client"
  [id table item]
  (if (= table "promotion")
    (sql/update!
     db
     (keyword table)
     (process-promotion (parse-string (str item) true))
     [(str "id=" id)])
    (sql/update! db (keyword table) (parse-string (str item) true) [(str "id=" id)])))

(defn delete-item
  "Delete an item with an id provided by the client"
  [table id]
  (sql/delete! db (keyword table) [(str "id=" id)]))  
Entity specific operations

Fine, we have our “generic operations” but we need to specify these functions with our specific entities, products and promotions in this case.

;; Product operations

(defn read-all-products []
  (read-all-items "product"))

(defn read-one-product [id]
  (read-one-item "product" id))

(defn update-product [id item]
  (update-item id "product" item))

(defn create-product [item]
  (create-one-item "product" item))

(defn delete-product [id]
  (delete-item "product" id))


;; Promotion operations

(defn read-all-promotions []
  (read-all-items "promotion"))

(defn read-one-promotion [id]
  (read-one-item "promotion" id))

(defn update-promotion [id item]
  (update-item id "promotion" item))

(defn create-promotion [item]
  (create-one-item "promotion" item))

(defn delete-promotion [id]
  (delete-item "promotion" id))

Resources(Liberator)

Resources in our project are like bridges between our models and routes. A resource follows the relevant requirements of our API design and specifies some behaviour according to Liberator resources specification.

What is Liberator?

When I wonder about Liberator, I usually think about a tool to helps us to represent our data as specific resources following certain HTTP specifications. We are using Liberator together with Ring and Compojure.

Why Liberator with Ring? Because liberator resources are ring handlers, so we can gain a higher abstraction layer over the details of HTTP. This way, we can avoid some code complexity and increase the code readability.

Why Liberator with compojure? Because our routes are managed via compojure, so we can connect specific routes with resource decisions, actions, handlers or declarations.

A resource in Liberator contains a set of keys. Each key has specific meanings and actions associated. A key can fall into one of these categories:

  • Decision
  • Handler
  • Action
  • Declaration
Decision

Decisions are used to take certain actions in response to specific events. The decision keys have a “?” at the end and their handler must be a boolean value.

For a complete list of decisions, you can check this link.

Handler

Handlers are keys to represent http status in Liberator. Every handler begins with “handler-“.

For a complete list of handlers, you can check this link.

Action

An action represents the current state of the client(PUT, DELETE, POST). Actions have an exclamation point in the end “!” to indicate that they are mutating the application’s state.

When an action is triggered, we can use the handle-created to return the result to the client.

For a complete list of actions, you can check this link

Declaration

Declaration are used to describe our resource’s capabilities. There are no syntax rules about declarations.

Resources in action

I think we have a better concept about Liberator and its components. Let see this in code.

We have two namespaces for our resources: liberator-service.resources.promotion contains every resource related with promotions and liberator-service.resources.product contains resources related with products.

Namespace: liberator-service.resources.promotion

(ns liberator-service.resources.promotion
  (:require [liberator.core
             :refer [defresource resource request-method-in]]
            [cheshire.core :refer [generate-string]]
            [liberator-service.models.db :refer 
             [read-all-promotions
              read-one-promotion
              update-promotion
              create-promotion
              delete-promotion]]))


(defresource promotion-all []
  :service-available? true
  :allowed-methods [:get]
  :handle-ok (fn [_] (generate-string (read-all-promotions)))
  :available-media-types ["application/json"])

(defresource promotion-one [id]
  :service-available? true
  :allowed-methods [:get]
  :handle-ok (fn [_] (generate-string (read-one-promotion id)))
  :available-media-types ["application/json"])

(defresource promotion-create [promotion]
  :service-available? true
  :allowed-methods [:post]
  :post! (fn [_] (create-promotion promotion))
  :handle-created promotion
  :available-media-types ["application/json"])

(defresource promotion-update [id promotion]
  :service-available? true
  :allowed-methods [:put]
  :put! (fn [_] (update-promotion id promotion))
  :handle-created promotion
  :available-media-types ["application/json"])

(defresource promotion-delete [id]
  :service-available? true
  :allowed-methods [:delete]
  :delete! (fn [_] (delete-promotion id))
  :available-media-types ["application/json"])

Namespace: liberator-service.resources.product

(ns liberator-service.resources.product
  (:require [liberator.core
             :refer [defresource resource request-method-in]]
            [cheshire.core :refer [generate-string]]
            [liberator-service.models.db :refer 
             [read-all-products
              read-one-product
              update-product
              create-product
              delete-product]]))


(defresource product-all []
  :service-available? true
  :allowed-methods [:get]
  :handle-ok (fn [_] (generate-string (read-all-products)))
  :available-media-types ["application/json"])

(defresource product-one [id]
  :service-available? true
  :allowed-methods [:get]
  :handle-ok (fn [_] (generate-string (read-one-product id)))
  :available-media-types ["application/json"])

(defresource product-create [product]
  :service-available? true
  :allowed-methods [:post]
  :post! (fn [_] (create-product product))
  :handle-created product 
  :available-media-types ["application/json"])

(defresource product-update [id product]
  :service-available? true
  :allowed-methods [:put]
  :put! (fn [_] (update-product id product))
  :handle-created product
  :available-media-types ["application/json"])

(defresource product-delete [id]
  :service-available? true
  :allowed-methods [:delete]
  :delete! (fn [_] (delete-product id))
  :available-media-types ["application/json"])

As you can see, we have used handlers, actions, decisions and declarations in this file. For example; we used the declaration available-media-types to specify that we want to use “application/json” format, we used the handler handle-ok to execute certain actions in response to successful operation, we used the decision service-available? to confirm that we want to expose this resource, and we used the action put! to define an update operation.

While I was making my research about Liberator, I saw resources with put! and delete! actions together in the same resource. I think this is ok because we can avoid a few lines of code, but for this approach I wanted to show Liberator thinking about expose the basic concepts for every component. So, If you want to try to improve the code that would be amazing. Remember I’m open to receive your feedback to get better everyday.

Routes

Routes in compojure provide a concise way of defining functions to handle http requests. A basic route structure look like this:

(GET "/book/:id" [id]
  (str "You want to see the id: " id))

The “GET” symbol is a macro provided by Compojure. This macro says the HTTP request method we want to use. There is a set of macros we can use, POST, PUT, DELETE, OPTIONS, PATCH and HEAD. All these macros are related with the corresponding HTTP method. If we want to match any HTTP method, we can use ANY.

The string “/book/:id” matches against the URI of the request. The “:id” substring will match any path up to the next “/”, and store the result in the id parameter.

After the evaluation of the expression GET “/book/:id”, compojure execute the next function evaluation. In this example executes (str “You want to see the id: “ id) to generate a response to the client.

In our project, we are using a context to make groups of common URI to simplify the code. You can see our routes in the following code snippets:

(ns liberator-service.routes.product
  (:require [compojure.core :refer :all]
            [liberator-service.resources.product :refer :all]))

(defroutes product-routes
  (context "/api/product" [] (defroutes noparam-routes
    (GET "/" [] (product-all))
    (POST "/" {body :body} (product-create (slurp body)))
      (context "/:id" [id] (defroutes param-routes
        (GET "/" [] (product-one id))
        (PUT "/" {body :body} (product-update id (slurp body)))
        (DELETE "/" [] (product-delete id)))))))
(ns liberator-service.routes.promotion
  (:require [compojure.core :refer :all]
            [liberator-service.resources.promotion :refer :all]))

(defroutes promotion-routes
  (context "/api/promotion" [] (defroutes noparam-routes
    (GET "/" [] (promotion-all))
    (POST "/" {body :body} (promotion-create (slurp body)))
      (context "/:id" [id] (defroutes param-routes
        (GET "/" [] (promotion-one id))
        (PUT "/" {body :body} (promotion-update id (slurp body)))
        (DELETE "/" [] (promotion-delete id)))))))

Starting up the project!

If you complete all the previous steps, then you can start the project. We can start the project using lein, with the following command:

lein ring server

This command will create a server using jetty at the port 3000. If you want to use another port just use:

leing ring server port

Where port is the number of the port that you wanna use.

Final thoughts

Congratulations! We did a great job, our API is ready!… Uhmmm but Don’t drink too much kool aid! We are still needing to build tests and authentication processes for our API. I will publish 2 posts related with testing and authentication to finish this job.

I hope these posts have helped you to understand the basic of the stack used used for this project Clojure/Ring/Compojure/Liberator/PostgreSQL. Remember you can clone or fork the project at source code anytime.

Happy coding! =)

Written on May 11, 2016