Building CRUD applications in Clojure
https://github.com/danielytics/diy-crud/
What do we need?
• Request Handler
• Routing
• Database Access
• Templating
DIY CRUD
• Request Handler
• Routing
• Database Access
• Templating
Ring
Compojure, bidi, pedestal, silk, ...
Yesql, korma, lots of others for noSQL databases
Enlive, enliven, selmer, hiccup, clostache, ...
Lots of options!
DIY CRUD
• Request Handler
• Routing
• Database Access
• Templating
Ring
Bidi
Yesql
Erinite/template
DIY CRUD
• Request Handler
• Routing
• Database Access
• Templating
Ring
Bidi
Yesql
Erinite/template
Why ring?
• Defacto standard
DIY CRUD
• Request Handler
• Routing
• Database Access
• Templating
Ring
Bidi
Yesql
Erinite/template
Why bidi?
• Routes are data
• Reversible (URL -> route, route -> URL)
• Clojure and Clojurescript
DIY CRUD
• Request Handler
• Routing
• Database Access
• Templating
Ring
Bidi
Yesql
Erinite/template
Why Yesql?
• Works with JDBC
• Write SQL as SQL
DIY CRUD
• Request Handler
• Routing
• Database Access
• Templating
Ring
Bidi
Yesql
Erinite/template
Why Erinite/template?
• Because I wrote it ;-)
• USP: template manipulation through data
• Will go into more detail later in this talk
DIY CRUD setup a project
$ lein new crud-talk; cd crud-talk; vim project.clj
:dependencies [[org.clojure/clojure “1.7.0”]
[ring “1.4.0-RC1”]
[bidi “1.20.0”]
[yesql "0.4.0“ :exclusions [instaparse]]
[instaparse “1.4.1”] ; For clj 1.7 support
[org.xerial/sqlite-jdbc "3.7.2"]
[hiccup "1.0.5"]
[erinite/template “0.2.0”]]
:plugins [[lein-ring "0.9.6"]]
:ring {:handler crud-talk.core/handler}
DIY CRUD create handler
$ vim src/crud-talk/core.clj
(ns crud-talk.core
(:require
[bidi.ring :as bidi-ring]
[yesql.core :refer [defqueries]]
[erinite.template.core :as t]
[hiccup.core :refer [html]]))
;; Other code goes here...
(def handler (bidi-ring/make-handler routes))
DIY CRUD create our routes
(def routes
["/" {"items"
{{:request-method :get} read-items
{:request-method :post} create-item}
["item/" :item-id]
{{:request-method :get} read-item
{:request-method :put} update-item
{:request-method :post} delete-item
{:request-method :delete} delete-item}}])
DIY CRUD create our routes
(def routes
["/" {"items"
{{:request-method :get} read-items
{:request-method :post} create-item}
["item/" :item-id]
{{:request-method :get} read-item
{:request-method :put} update-item
{:request-method :post} delete-item
{:request-method :delete} delete-item}}])
So we can test in browser without using JS
DIY CRUD read a list of items
-- name: get-all-items
-- Read a summary list of all items in database.
SELECT id, name, quantity
FROM items
DIY CRUD create database queries
(def db-spec {:classname "org.sqlite.JDBC"
:subprotocol "sqlite"
:subname "db.sqlite"})
(defqueries “queries.sql”)
DIY CRUD create database queries
Make sure you have a database setup:
$ sqlite db.sqlite
sqlite> create table items (id int, name text, description text, qunantity int);
sqlite> insert into items values (1, ‘Test item’, ‘A test item. The first one’, 5);
sqlite> insert into items values (2, ‘Another item’, ‘A test item. The second one’, 26);
sqlite> insert into items values (3, ‘Item’, ‘The third item’, 3);
DIY CRUD create template
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit"} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
DIY CRUD transform template
(def item-list-transformations
{[:.items] [:clone-for :items]
[:.items :.id] [:content :id]
[:.items :.name] [:content :name]
[:.items :.quantity] [:content :quantity]
[:.items :form] [:set-attr :action :url]})
DIY CRUD compile template
(def item-list
(t/compile-template
item-list-template
item-list-transformation))
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD the data
({:id 1
:name “Test item”
:quantity 5}
{:id 2
:name “Another item”
:quantity 26}
{:id 3
:name “Item”
:quantity 3})
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD the data
({:id 1
:name “Test item”
:url “/item/1”
:quantity 5}
{:id 2
:name “Another item”
:url “/item/2”
:quantity 26}
{:id 3
:name “Item”
:url “/item/3”
:quantity 3})
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD the data
{:items ({:id 1
:name “Test item”
:url “/item/1”
:quantity 5}
{:id 2
:name “Another item”
:url “/item/2”
:quantity 26}
{:id 3
:name “Item”
:url “/item/3”
:quantity 3})
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD the data
[:div {}
[:h1 {} "Item List"]
[:table {:class “items”}
[:tr {}
[:td {:class “id”} 1]
[:td {:class “name”} “Test Item”]
[:td {:class “quantity”} 5]
[:td {}
[:form {:action “/item/1” :method “post”}
[:button {:type "submit“} "delete"]]]]]
...
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD the data
<div>
<h1>Item List</h1>
<table class=“items”>
<tr>
<td class=“id”> 1</td>
<td class=“name”>Test Item</td>
<td class=“quantity”>5</td>
<td>
<form action=“/item/1” method=“post”>
<button type="submit“>delete</button>
</form>
</td>
</tr>
...
DIY CRUD item list handler
(defn read-items [request]
(let [items (map
#(assoc % :url (str "/item/" (:id %)))
(get-all-items db-spec))]
{:status 200
:body (html (item-list {:items items}))}))
DIY CRUD test run!
$ lein ring server
Now open localhost:3001/items
DIY CRUD Erinite/template crash course
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit“} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
DIY CRUD Erinite/template crash course
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit“} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
[:.items] [:clone-for :items]
DIY CRUD Erinite/template crash course
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit“} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
[:.items] [:clone-for :items]
DIY CRUD Erinite/template crash course
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit"} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
[:.items] [:clone-for :items]
{:items ({:id 1
:name “Test item”
:url “/item/1”
:quantity 5}
{:id 2
:name “Another item”
:url “/item/2”
:quantity 26}
{:id 3
:name “Item”
:url “/item/3”
:quantity 3})
DIY CRUD Erinite/template crash course
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit“} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
[:.items :.id] [:content :id]
DIY CRUD Erinite/template crash course
(def item-list-template
[:div
[:h1 "Item List"]
[:table.items
[:tr
[:td.id] [:td.name] [:td.quantity]
[:td [:form {:method “post”} [:button {:type "submit“} "delete"]]]]]
[:hr] [:div "New item"]
[:form#new {:action "/items“ :method "post"}
[:input.name {:type "text"}]
[:input.description {:type "text"}]
[:input.quantity {:type "text"}]
[:button {:type "submit"} "add"]]])
[:.items :.id] [:content :id]
{:id 1
:name “Test item”
:url “/item/1”
:quantity 5}
DIY CRUD detour: erinite/template-stylesheets
Detour Time!
Erinite/template comes with a companion library: erinite/template-stylesheets
DIY CRUD detour: erinite/template-stylesheets
Detour Time!
Erinite/template comes with a companion library: erinite/template-stylesheets
{[:.items] [:clone-for :items][:.items :.id] [:content :id][:.items :.name] [:content :name][:.items :.quantity] [:content :quantity][:.items :form] [:set-attr :action :url]}
DIY CRUD detour: erinite/template-stylesheets
Detour Time!
Erinite/template comes with a companion library: erinite/template-stylesheets
{[:.items] [:clone-for :items][:.items :.id] [:content :id][:.items :.name] [:content :name][:.items :.quantity] [:content :quantity][:.items :form] [:set-attr :action :url]}
DIY CRUD detour: erinite/template-stylesheets
.items {clone-for: items;
}.items .id {
content: id;}.items .name {
content: name;}.items .quantity {
content: quantity;}.items form {
set-attr: action url;}
DIY CRUD detour: erinite/template-stylesheets
.items {clone-for: items;
}.items .id {
content: id;}.items .name {
content: name;}.items .quantity {
content: quantity;}.items form {
set-attr: action url;}
Now your designers can update yourtemplate transformation selectors!
DIY CRUD deleting items
-- name: delete-item!
-- Delete a specific item from the database.
DELETE FROM items
WHERE id = :id
DIY CRUD deleting items
(defn delete-item [request]
(let [item-id (get-in request
[:route-params :item-id])]
(delete-item! db-spec item-id)
{:status 302
:headers {"Location" "/items"}}))
DIY CRUD deleting items
(defn delete-item [request]
(let [item-id (get-in request
[:route-params :item-id])]
(delete-item! db-spec item-id)
{:status 302
:headers {"Location" "/items"}}))
DIY CRUD create our routes
(def routes
["/" {"items"
{{:request-method :get} read-items
{:request-method :post} create-item}
["item/" :item-id]
{{:request-method :get} read-item
{:request-method :put} update-item
{:request-method :post} delete-item
{:request-method :delete} delete-item}}])
DIY CRUD deleting items
(defn delete-item [request]
(let [item-id (get-in request
[:route-params :item-id])]
(delete-item! db-spec item-id)
{:status 302
:headers {"Location" "/items"}}))
DIY CRUD deleting items
(defn delete-item [request]
(let [item-id (get-in request
[:route-params :item-id])]
(delete-item! db-spec item-id)
{:status 302
:headers {"Location" "/items"}}))
DIY CRUD
...and so on for create and update.
Where to next?
Liberator
Define resources as conditions and actions in a state machine.
Very flexible
Makes it easy to be RFC compliant
But is lower level than I’d like
Where to next?
Yada
JUXT’s new Liberator competitor
Simpler and higher level than Liberator
Built-in swagger support
Where to next?
Erinite/crud
There is no good out-of-the-box CRUD library than I know of. So I plan on making one.
https://github.com/Erinite/crud
Where to next?
Erinite/crud
Doesn’t exist yet, so I’m taking feature requests
Where to next?
Erinite/crud
The plan so far:
• Support any database (by implementing a protocol)
• Data-driven handlers (you define your resources as data, library does the rest)
• Just a handler, so you can mount it at any route you want
Questions?