Basic Web Application in Clojure
September 21, 2013
Abstract: This tutorial covers the creation of a basic CRUD web application (read: blog) using Clojure. The technologies covered here include Compojure, Ring, Hiccup, JDBC, MySQL, Apache, and Tomcat. Development is done on Mac OS X 10.8.5, and deployment is done on a VPS with Ubuntu 12.04 installed.
Table of Contents
Introduction
If you want to learn a language that’s expressive and that will stand the test of time, a functional language is probably the way to go. At least, that’s the case Paul Graham seems to be making. Some even say that understanding Lisp is like achieving enlightenment.
So I decided to give it a try. Between the functional languages Haskell and Clojure, I ended up picking Clojure because of its practicality, growing popularity, and because it runs on JVM, which means, pretty much everywhere.
But because Clojure is still young, the documentation could still use a lot of improvement. There are quite a few articles on how to create a web application using Clojure, but they all cover different combinations of libraries. And because it is a young language, packages are still in flux (e.g., Noir has been deprecated, but it has also been incorporated into the Compojure), so articles can quickly become outdated. All of these things can easily overwhelm a newcomer like me, and cause one to spend hours on tangential results on Google. So I’m joining the fray to make an already messy landscape even more messy. Just kidding. What I really hope to achieve here is to create a guide that I would have appreciated when I started out (about a week ago).
Caveat
Since I just started using Clojure, I probably don’t have best practices down. Furthermore, I didn’t fully DRY out my code, mostly because adding additional layers of abstraction can be confusing to a beginner. In general, as good practice, try to decompose the functions as much as possible.
Objective
The goal is to help new users of Clojure set up a simple CRUD web application from the ground up (or nearly so). Because I want people to get a feel for what making a Clojure web app is like, I won’t focus on optimization, validation, tests, or security. Once you grasp how everything fits together, I have no doubt you will be able to implement those things on your own.
This tutorial will try to keep code and explanations as short as sufficient and as long as necessary. I will assume you have basic knowledge of Clojure syntax (i.e., you know the difference between a list and a vector, you know what #(myfunction %)
means, you know basic Clojure functions like map
, str
, etc.
Environment
I’m using Mac OS X 10.8.5 for development, and a VPS running Linux (Ubuntu 12.04) for production. You can use any text editor you want. If you want a REPL without dealing with configuration, Light Table has Instarepl, which is a good basic REPL.
We need Homebrew. It’s really easy to set up.
$ ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"
Next, use brew
to install leiningen
, which is almost like the rails
script for ruby
, except probably even more comprehensive.
$ brew install leiningen
For reference, a summary of the version numbers of my software is listed here.
Mac OS X 10.8.5
Homebrew 0.9.4
Leiningen 2.3.2
Clojure 1.5.1 (included with the latest version of Leiningen)
Create Project
$ lein new compojure myblog
Compojure does the routing (parsing of the path in your URL). Verify that the server works by running the following:
$ lein ring server
Go to http://localhost:3000 to make sure it works.
The root of the source tree (or the myblog
folder) contains a file called project.clj
. This is where you put dependency information. We’ll be adding things here as we build up the project.
The other thing to pay attention to right now is the myblog/src/myblog
folder, which contains the Clojure source files for your project. There should just be one file at this point: handler.clj
, which contains your routes.
Development
Getting a basic page up and running
If you look at handler.clj
, you’ll find a route to the root of your site, already made for you:
(GET "/" [] "Hello World")
Changing “Hello World” to whatever simple string you want is trivial. But we want actual HTML, so we’ll use Hiccup to generate our HTML from Clojure data structures. I don’t want to clutter up handler.clj
, so I want to create a new file.
Every file has its own namespace, so if you go the route of having separate files, you have to declare a namespace for each of them. So let’s create a new file called views.clj
and fill it with code that will display an HTML page.
myblog/src/myblog/views.clj
(ns myblog.views
(:require [hiccup.core :refer (html)]))
(defn layout [title & content]
(html
[:head [:title title]]
[:body content]))
(defn main-page []
(layout "My Blog"
[:h1 "My Blog"]
[:p "Welcome to my page"]))
As you can see, we have to “require” hiccup.core
to use the html
function, which is the only function we’re importing. It converts a nested vector to HTML tags.
We created a helper function called layout
that takes care of setting up the page framework (if you’re familiar with Ruby on Rails, this is analogous to application.html.erb
). The ampersand means content
will collect any arguments beyond the first one (which is the title
) into a Clojure list.
We then created a function called main-page
that will be called by our routes handler. When using defn
, the empty brackets after the function name are required.
Before we can call this function from our handler, we have to add [myblog.views :as views]
to our list of requirements to import all the functions in views.clj
and to make an alias that makes our referencing easier. With :as
, we don’t have to write myblog.views/function-name
; we can just write views/function-name
.
myblog/src/myblog/handler.clj
(ns myblog.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[myblog.views :as views]))
...
Now we replace "Hello World"
with (views/main-page)
, like this:
(GET "/" [] (views/main-page))
You can refresh your browser to see it in action.
Administrator Section
Now we need to create a protected part of the website for the administrator. In this section, we’ll organize the routes in preparation for our authentication system, but we won’t apply security just yet.
First, move your current routes (except for the not-found
route) into a new route function called public-routes
.
Then add a protected-routes
function, with a route to "/admin"
. Let’s also call the function generating that view views/admin-blog-page
, which we haven’t written yet.
Finally, we simplify app-routes
. Your handler.clj
should look like this:
myblog/src/myblog/handler.clj
(ns ...
(defroutes public-routes
(GET "/" [] (views/main-page))
(route/resources "/"))
(defroutes protected-routes
(GET "/admin" [] (views/admin-blog-page)))
(defroutes app-routes
public-routes
protected-routes
(route/not-found "Not Found"))
(def app ...
In views.clj
, we have to do three things:
- Add the
admin-blog-page
function, which will generate the admin’s page for us. Fortunately, we’ve already writtenlayout
, which will help to keep our code compact. - Write a helper
post-summary
that will display a summary of a post. - Add
myblog.posts
to the requirements, even though we have not written it yet. Theposts
file will take care of interacting with the database.
myblog/src/myblog/views.clj
(ns myblog.views
(:require [hiccup.core :refer (html)]
[myblog.posts :as posts]))
(defn layout ...
(defn main-page ...
; Post is a map corresponding to a record from the database
(defn post-summary [post]
(let [id (:id post)
title (:title post)
body (:body post)
created_at (:created_at post)]
[:section
[:h3 title]
[:h4 created_at]
[:section body]
[:section.actions
[:a {:href (str "/admin/" id "/edit")} "Edit"] " / "
[:a {:href (str "/admin/" id "/delete")} "Delete"]]]))
(defn admin-blog-page []
(layout "My Blog - Administer Blog"
[:h1 "Administer Blog"]
[:h2 "All my posts"]
[:a {:href "/admin/add"} "Add"]
(map #(post-summary %) (posts/all))))
We will define posts/all
and create the /admin/add
page in the next section.
CRUD
I assume you know how to set up a MySQL database on your computer. You can use phpMyAdmin, the command line, or if you’re using OS X, Sequel Pro. Create a database and a user and give that user access to the database you just created. For purposes of illustration, I will call the database myblog
, and create a user/password pair (user1
, pass1
). Also create a table called posts
, with the following fields and types:
id, INT
title, VARCHAR(255)
body, MEDIUMTEXT
created_at, DATETIME
updated_at, DATETIME
I would recommend creating a mock record in posts
for the next section.
Before we build each part of CRUD one by one, let’s add some dependency information to project.clj
so that we can talk to the MySQL database.
myblog/project.clj
...
:dependencies [[ ... ]
[ ... ]
[org.clojure/java.jdbc "0.3.0-alpha5"]
[mysql/mysql-connector-java "5.1.25"]]
...
Let’s now create posts.clj
. It will contain our posts
model.
myblog/src/myblog/posts.clj
(ns myblog.posts
(:refer-clojure :exclude [get])
(:require [clojure.java.jdbc :as j]
[clojure.java.jdbc.sql :as s]))
(def mysql-db {:subprotocol "mysql"
:subname "//localhost:3306/myblog"
:user "user1"
:password "pass1"
:zeroDateTimeBehavior "convertToNull"})
Notice I excluded the get
function from the Clojure core, in preparation for having my own function called get
in this namespace. My friend tells me that best practice is to use a prefix (so instead of calling my new function get
, I should use sql-get
), but this example shows you how to resolve function name conflicts in case you need to use it. I also included the two JDBC dependencies, and put my database connection information into a function with zeroDateTimeBehavior
set to "convertToNull"
so that “0000-00-00 00:00:00” will be returned by JDBC as NULL values.
Read
Now we add the all
function that we are calling from views.clj
, as well as a function for retrieving a specific post by its ID.
myblog/src/myblog/posts.clj
(ns ...
(def mysql-db ...
(defn all []
(j/query mysql-db
(s/select * :posts)))
(defn get [id]
(first (j/query mysql-db
(s/select * :posts (s/where {:id id})))))
At this point, the site is in a viewable state, where there are no undefined functions and all the dependencies have been met (however, not all the links go somewhere yet). Since we added dependencies since the last time the server was started, we need to interrupt the current instance of the server using control-C (C) and run lein ring server
again.
If you made a mock post as suggested earlier, you can go to http://localhost:3000/admin and you will see your mock post there with two links “Edit” and “Delete.”
Create
To create a post, we need a function in the model that does the creating of a new record. While we’re doing that, we also need a symbol (defined by def
instead of defn
) to feed the MySQL database the current time. If you’re using defn
, make sure to include the empty brackets after the function name.
myblog/src/myblog/posts.clj
(ns ...
(def mysql-db ...
(def now
(str (java.sql.Timestamp. (System/currentTimeMillis))))
(defn all ...
(defn get ...
(defn create [params]
(j/insert! mysql-db :posts (merge params {:created_at now :updated_at now})))
Here, params
is hash that contains the data of the fields from whatever form we’re going to post from.
We also need a form for us to create a new post. So include [hiccup.form :as f]
in the namespace for views.clj
to be able to access some form helpers. Then create a new function in views.clj
:
myblog/src/myblog/views.clj
(ns myblog.views
(:require [hiccup.core :refer (html)]
[hiccup.form :as f] ; <-- add this
[myblog.posts :as posts]))
...
(defn add-post []
(layout "My Blog - Add Post"
(list
[:h2 "Add Post"]
(f/form-to [:post "/admin/create"]
(f/label "title" "Title")
(f/text-field "title") [:br]
(f/label "body" "Body") [:br]
(f/text-area {:rows 20} "body") [:br]
(f/submit-button "Save")))))
The form will use POST to issue a call to the route /admin/create
. We now have to update our handler.clj
with three things:
- create the
/admin/create
route, - require
myblog.posts
since in our/admin/create
route, we will callposts/create
, and - require
ring.util.response
to use its redirection function after callingposts/create
.
myblog/src/myblog/handler.clj
(ns myblog.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[myblog.views :as views]
[myblog.posts :as posts] ; <-- add this line
[ring.util.response :as resp])) ; <-- and this line
...
(defroutes protected-routes
(GET "/admin" [] (views/admin-blog-page))
(GET "/admin/add" [] (views/add-post))
(POST "/admin/create" [& params]
(do (posts/create params)
(resp/redirect "/admin"))))
Notice I’m doing two things in the POST handler by using do
: I first call the posts/create
function to insert the record into the database, and then I redirect back to the main admin page.
Update
We add a function to our posts
model:
myblog/src/myblog/posts.clj
...
(defn save [id params]
(j/update! mysql-db :posts params (s/where {:id id})))
We make a page for the edit form:
myblog/src/myblog/views.clj
...
(defn edit-post [id]
(layout "My Blog - Edit Post"
(list
(let [post (posts/get id)]
[:h2 (str "Edit Post " id)]
(f/form-to [:post "save"]
(f/label "title" "Title")
(f/text-field "title" (:title post)) [:br]
(f/label "body" "Body") [:br]
(f/text-area {:rows 20} "body" (:body post)) [:br]
(f/submit-button "Save"))))))
We add two routes, one for the edit page, and one for the action to update the record (which we’ll call save
). Your protected-routes should look like this now:
myblog/src/myblog/handler.clj
(defroutes protected-routes
(GET "/admin" [] (views/admin-blog-page))
(GET "/admin/add" [] (views/add-post))
(POST "/admin/create" [& params]
(do (posts/create params)
(resp/redirect "/admin")))
(GET "/admin/:id/edit" [id] (views/edit-post id))
(POST "/admin/:id/save" [& params]
(do (posts/save (:id params) params)
(resp/redirect "/admin"))))
Delete
Delete is a lot easier to write than the others:
myblog/src/myblog/posts.clj
...
(defn delete [id]
(j/delete! mysql-db :posts (s/where {:id id})))
Update the routes by adding this to the protected-routes
function:
myblog/src/myblog/handler.clj
...
(GET "/admin/:id/delete" [id]
(do (posts/delete id)
(resp/redirect "/admin")))
What about REST?
To keep things simple, I made the delete action use GET and the save action use POST. Ideally, we would use DELETE and PUT, but that requires some more abstractions, and may be a topic of a future tutorial.
Authentication
Now that we have the basic functionality of the website down, let’s make sure only someone who is authorized can access the admin section.
Add [ring-basic-authentication "1.0.2"]
to the dependency list in project.clj
:
myblog/project.clj
(defproject myblog "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.5.1"]
[compojure "1.1.5"]
[org.clojure/java.jdbc "0.3.0-alpha5"]
[mysql/mysql-connector-java "5.1.25"]
[ring-basic-authentication "1.0.2"]] ; <-- add this line
:plugins [[lein-ring "0.8.5"]]
:ring {:handler myblog.handler/app}
:profiles
{:dev {:dependencies [[ring-mock "0.1.5"]]}})
Modify handler.clj
as follows:
myblog/src/myblog/handler.clj
(ns myblog.handler
(:require ...
[ring.middleware.basic-authentication :refer :all]))
(defn authenticated? [name pass]
(and (= name "user")
(= pass "pass")))
...
(defroutes app-routes
public-routes
(wrap-basic-authentication protected-routes authenticated?)
(route/not-found "Not Found"))
That’s it!
Full Code Listing
myblog/project.clj
(defproject myblog "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.5.1"]
[compojure "1.1.5"]
[org.clojure/java.jdbc "0.3.0-alpha5"]
[mysql/mysql-connector-java "5.1.25"]
[ring-basic-authentication "1.0.2"]]
:plugins [[lein-ring "0.8.5"]]
:ring {:handler myblog.handler/app}
:profiles
{:dev {:dependencies [[ring-mock "0.1.5"]]}})
myblog/src/myblog/handler.clj
(ns myblog.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[myblog.views :as views]
[myblog.posts :as posts]
[ring.util.response :as resp]
[ring.middleware.basic-authentication :refer :all]))
(defn authenticated? [name pass]
(and (= name "user")
(= pass "pass")))
(defroutes public-routes
(GET "/" [] (views/main-page))
(route/resources "/"))
(defroutes protected-routes
(GET "/admin" [] (views/admin-blog-page))
(GET "/admin/add" [] (views/add-post))
(POST "/admin/create" [& params]
(do (posts/create params)
(resp/redirect "/admin")))
(GET "/admin/:id/edit" [id] (views/edit-post id))
(POST "/admin/:id/save" [& params]
(do (posts/save (:id params) params)
(resp/redirect "/admin")))
(GET "/admin/:id/delete" [id]
(do (posts/delete id)
(resp/redirect "/admin"))))
(defroutes app-routes
public-routes
(wrap-basic-authentication protected-routes authenticated?)
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
myblog/src/myblog/views.clj
(ns myblog.views
(:require [hiccup.core :refer (html)]
[hiccup.form :as f]
[myblog.posts :as posts]))
(defn layout [title & content]
(html
[:head [:title title]]
[:body content]))
(defn main-page []
(layout "My Blog"
[:h1 "My Blog"]
[:p "Welcome to my page"]))
; Post is a map corresponding to a record from the database
(defn post-summary [post]
(let [id (:id post)
title (:title post)
body (:body post)
created_at (:created_at post)]
[:section
[:h3 title]
[:h4 created_at]
[:section body]
[:section.actions
[:a {:href (str "/admin/" id "/edit")} "Edit"] " / "
[:a {:href (str "/admin/" id "/delete")} "Delete"]]]))
(defn admin-blog-page []
(layout "My Blog - Administer Blog"
[:h1 "Administer Blog"]
[:h2 "All my posts"]
[:a {:href "/admin/add"} "Add"]
(map #(post-summary %) (posts/all))))
(defn add-post []
(layout "My Blog - Add Post"
(list
[:h2 "Add Post"]
(f/form-to [:post "/admin/create"]
(f/label "title" "Title")
(f/text-field "title") [:br]
(f/label "body" "Body") [:br]
(f/text-area {:rows 20} "body") [:br]
(f/submit-button "Save")))))
(defn edit-post [id]
(layout "My Blog - Edit Post"
(list
(let [post (posts/get id)]
[:h2 (str "Edit Post " id)]
(f/form-to [:post "save"]
(f/label "title" "Title")
(f/text-field "title" (:title post)) [:br]
(f/label "body" "Body") [:br]
(f/text-area {:rows 20} "body" (:body post)) [:br]
(f/submit-button "Save"))))))
myblog/src/myblog/posts.clj
(ns myblog.posts
(:refer-clojure :exclude [get])
(:require [clojure.java.jdbc :as j]
[clojure.java.jdbc.sql :as s]))
(def mysql-db {:subprotocol "mysql"
:subname "//localhost:3306/myblog"
:user "jluo"
:password "podunkoregon"
:zeroDateTimeBehavior "convertToNull"})
(defn all []
(j/query mysql-db
(s/select * :posts)))
(defn get [id]
(first (j/query mysql-db
(s/select * :posts (s/where {:id id})))))
(def now
(str (java.sql.Timestamp. (System/currentTimeMillis))))
(defn create [params]
(j/insert! mysql-db :posts (merge params {:created_at now :updated_at now})))
(defn save [id params]
(j/update! mysql-db :posts params (s/where {:id id})))
(defn delete [id]
(j/delete! mysql-db :posts (s/where {:id id})))
Deployment
There are a myriad of ways to set up your Clojure deployment. The way I’m presenting is not the best, but it’s transparent, and it allows you to host multiple sites on port 80 running on different languages (the real reason I’m using Apache).
Run in your project directory, run
$ lein ring uberwar
The .war file will be located in your myblog/target
directory.
Server Setup
I assume you are running Ubuntu 12.04 and you have Apache installed. I also assume you have a JVM installed.
We need to install Tomcat.
$ sudo apt-get install tomcat7-user
We’re going to use a private instance of Tomcat.
Create a private instance directory. I created mine in my home directory.
$ tomcat7-instance-create my-tomcat-instance
Now edit server.xml
in ~/my-tomcat-instance/conf
and uncomment the following line:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
Make sure mod_jk
is installed
$ sudo apt-get install libapache2-mod-jk
Now create a workers.properties
file for Apache:
$ sudo nano /etc/apache2/workers.properties
Add the following to the file:
# Define 1 real worker using ajp13
worker.list=worker1
# Set properties for worker (ajp13)
worker.worker1.type=ajp13
worker.worker1.host=localhost
worker.worker1.port=8009
Make sure mod_jk
is enabled.
$ sudo a2enmod jk
In your Apache virtual host settings (inside /etc/apache2/sites-available/
), add (or edit):
<VirtualHost *:80>
ServerAdmin admin@domain.com
ServerName test.domain.com
ServerAlias test.domain.com
JkMount /* worker1
</VirtualHost>
Uploading
Now upload the .war file in myblog/target
on your development machine to the server, and place it in ~/my-tomcat-instance/webapps
. Finally, rename it to ROOT.war
.
To start up your Tomcat instance,
$ my-instance/bin/startup.sh
Tomcat should automatically detect your .war file and launch the application.
Make sure you have your production database running with the user and database information specified in your posts.clj
file, or else you’ll get an internal server error when you try to access the admin part of the site!
If you don’t plan to use the root of the subdomain to run your app, then replace JkMount /* worker1
in the virtual host settings with JkMount /myblog* worker1
, and upload your file as myblog.war
instead of ROOT.war
.
Conclusion
Clojure is a maturing language. Therefore, documentation at this point is kind of all over the place. It took several days to figure out the basics. Most of the time was spent on logistical things and non-obvious “gotchas” that prevented me from really enjoying the language. Because the Clojure ecosystem is still changing, this tutorial will undoubtedly be outdated within a few years, but I do hope it helps someone in the meantime. E-mail me with comments/corrections/constructive criticisms.
References
- Clojure Cheatsheet
- Clojure Programming/Examples/JDBC Examples
- java.jdbc API Reference
- Installing Tomcat 7 and Apache2 with mod_jk on Ubuntu 12.04
- Apache Tomcat: Using private instances
Thanks to Wei for proofreading this tutorial!