Saturday, October 17, 2015

REST api using Clojure and MySql. Is Clojure the most productive, robust language on the planet?

Updated 2019 code: https://gitlab.com/danbunea/production-ready-clojure

In my life, I have written a lot of web applications, using Java, then .NET, PHP and lately REST API;s using Python. I thought there cannot be anything to match Python productivity using Flask and SqlAlchemy until today.

Making a REST api in Clojure using Ring/Compojure and SqlKorma 

There is a great package manager called Leiningen (http://leiningen.org). To create a new web application, in a Terminal:

> lein new compojure todoapp2
> cd todoapp2

Update 2019

> lein new compojure-api asado +clojure-test
> cd asado

Now you have the skeleton application with a pretty known structure. Now we need to configure which packages will be used, editing package.clj:

(defproject todoapp2 "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :min-lein-version "2.0.0"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [compojure "1.3.1"]
                 [ring/ring-core "1.3.2"]
                 [ring/ring-json "0.3.1"]
                 [ring/ring-defaults "0.1.4"]
                 [korma "0.3.0-RC5"]
                 [mysql/mysql-connector-java "5.1.6"]]
  :plugins [[lein-ring "0.8.13"]]
  :ring {:handler todoapp2.handler/app}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [ring-mock "0.1.5"]]}})

Update 2019:


(defproject asado "0.1.0-SNAPSHOT"  :description "FIXME: write description"  :dependencies [[org.clojure/clojure "1.10.0"]
                 [metosin/compojure-api "2.0.0-alpha30"]
                 [ring/ring "1.6.3"]
                 [compojure "1.6.1"]
                 [manifold "0.1.8"]
                 [metosin/spec-tools "0.9.2"]

                 ;database                 [korma "0.4.3"]
                 [mysql/mysql-connector-java "8.0.12"]
                 ]
  :ring {:handler asado.handler/app}
  :uberjar-name "server.jar"  :profiles {:dev {:dependencies [[javax.servlet/javax.servlet-api "3.1.0"]
                                 [ring/ring-mock "0.3.2"]]
                  :plugins [[lein-ring "0.12.5"]]}})

Now to add all dependencies:

> lein deps 

Basically we need the json package, sqlkorma and the mysql JDBC driver. All installed. 

In MySql we'll create a database todo, where we create a table items, with id, title (varchar), is_complete (tinyint).

CREATE TABLE `items` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `title` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `is_complete` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Now let's create a database.clj file where we configure the database connection details:

(ns todoapp2.database
  (:require [korma.db :as korma]))

(def db-connection-info (korma/mysql 
  {:classname "com.mysql.jdbc.Driver"
   :subprotocol "mysql"
   :user "root"
   :subname "//localhost:3306/todo"}))

; set up korma
(korma/defdb db db-connection-info)

Update 2019:


(ns asado.database
  (:require [korma.db :as korma]))

(def db-connection-info (korma/mysql                          {:classname "com.mysql.cj.jdbc.Driver"                           :subprotocol "mysql"                           :user "root"
                           :subname "//localhost:3307/todo"}))

; set up korma(korma/defdb db db-connection-info)

Now let's write the database access functions, in a new file: query.clj

(ns todoapp2.query
  (:require [todoapp2.database]
            [korma.core :refer :all]))

(defentity items)

(defn get-todos []
  (select items))

(defn add-todo [title]
  (insert items
          (values {:title title})))

(defn delete-todo [id]
  (delete items
          (where {:id [= id]})))

(defn update-todo [id title is-complete]
  (update items
          (set-fields {:title title
                       :is_complete is-complete})
          (where {:id [= id]})))

(defn get-todo [id]
  (first
    (select items
          (where {:id [= id]}))))

Update 2019:


(ns asado.query
  (:require [asado.database :refer [db]]
            [korma.core :refer :all]
            [schema.core :as s]))

(s/defschema TitleBody
             {:title s/Str})

(s/defschema TodoBody
             {:id s/Int
              :title s/Str
              :is_complete s/Bool})


(defentity items)

(defn get-todos []
  (select items))

(defn add-todo [title]
  (insert items
          (values {:title title})))

(defn delete-todo [id]
  (delete items
          (where {:id id})))

(defn update-todo [id title is-complete]
  (update items
          (set-fields {:title       title
                       :is_complete is-complete})
          (where {:id id})))

(defn get-todo [id]
  (first    (select items
            (where {:id id}))))

All done. SqlKorma is extremely easy to use, very composable.

Ok, now let's write the REST services:

(ns todoapp2.handler
  (:require [compojure.core :refer :all]
  [compojure.handler :as handler]
            [compojure.route :as route]
            [ring.middleware.json :as json]
            [ring.util.response :refer [response]]
            [todoapp2.query :refer :all]))

(defroutes app-routes
  (GET "/api/todos" []
       (response (get-todos)))
  (GET "/api/todos/:id" [id]
       (response (get-todo (Integer/parseInt id))))
  (POST "/api/todos" [title]
       (response (add-todo title)))
  (PUT "/api/todos/:id" [id title is_complete]
       (response (update-todo (Integer/parseInt id) title is_complete)))
  (DELETE "/api/todos/:id" [id]
        (response (delete-todo (Integer/parseInt id))))
  (route/resources "/")
  (route/not-found "Not Found"))

(def app
  (-> (handler/api app-routes)
      (json/wrap-json-params)
      (json/wrap-json-response)))

Update 2019, using compojure-api:

(ns asado.handler
  (:require [compojure.api.sweet :refer :all]
            [ring.util.http-response :refer :all]
            [schema.core :as s]
            [asado.query :as q]))


(def app
  (api    {:swagger     {:ui "/"      :spec "/swagger.json"      :data {:info {:title "Asado"                    :description "Compojure Api example"}
             :tags [{:name "api", :description "some apis"}]}}}

    (context "/api" []
      :tags ["api"]

      (GET "/todos" []
        (ok (q/get-todos )))
      (GET "/api/todos/:id" []
        :path-params [id :- s/Int]
        (ok (q/get-todo id)))
      (POST "/api/todos" []
        :body [title-body q/TitleBody]
        (let [{:keys [title]} title-body]
          (ok (q/add-todo title))))
      (PUT "/api/todos" []
        :body [todo-body q/TodoBody]
        (let [{:keys [id title is_complete]} todo-body]
          (ok {:updated (q/update-todo id title is_complete)})))
      (DELETE "/api/todos/:id" []
        :path-params [id :- s/Int]
        (ok {:deleted (q/delete-todo id)}))

      )))




Starting the server:

>lein ring start

Using a tool like Advanced REST Client plugin for Chrome will allow you to use the API:




And accessing http://localhost:3000/api/todos will show you what you created.

Update 2019



Conclusion


Pretty awesome!