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
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:
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;
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:
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:
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:
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:
Conclusion
Pretty awesome!
1 comment:
You are right.Clojure is the most productive language on the planet with all loaded features out of the box and yet simple and elegant.
Post a Comment