Developing clojure
Posted on April 8th, 2016
As part of work, I ended up having to learn and use clojure for something more than my pet projects. I have to say it grows on me every time. Ring and compojure make it quite pleasant to develop web applications. The smallest example of a ring web application I could come up with was the following:
(ns samples.minimal
(:gen-class)
(:require
[org.httpkit.server :refer [run-server]]
[environ.core :refer [env]]))
(def handler
(fn [request]
{:status 200 :headers {"Content-Type" "text/html"} :body "Hello Minimal World!"}))
(defn -main [& [port]] ;; entry point, lein run will pick up and start from here
(let [p (Integer. (or port (env :port) 5000))]
(run-server handler {:port p})))
All this gives us is the following:
> curl 'http://localhost:3000/' -I -s
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:10 GMT
> curl 'http://localhost:3000/' -s
Hello Minimal World!
> curl 'http://localhost:3000/testing' -I -s
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:10 GMT
> curl 'http://localhost:3000/testing' -s
Hello Minimal World!
> curl 'http://localhost:3000/js/template.js' -I -s
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:11 GMT
> curl 'http://localhost:3000/js/template.js' -s
Hello Minimal World!
> curl 'http://localhost:3000/css/template.css' -I -s
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:11 GMT
> curl 'http://localhost:3000/css/template.css' -s
Hello Minimal World!
Ring's middlewares are the cherry on top of the cake. They are super powerful and feel very intuitive despite my expectations. There is a default ring middleware project that adds a bunch of pretty common useful middleware (like CSRF, content headers, caching, static assets like js and css, etc):
(ns samples.with-middleware
(:gen-class)
(:require
[org.httpkit.server :refer [run-server]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults api-defaults]]
[environ.core :refer [env]]))
(def handler
(fn [request]
{:status 200 :headers {"Content-Type" "text/html"} :body "Hello World with middleware!"}))
(def app
(wrap-defaults handler site-defaults))
(defn -main [& [port]] ;; entry point, lein run will pick up and start from here
(let [p (Integer. (or port (env :port) 5000))]
(run-server app {:port p})))
Now our results are as follow:
> curl 'http://localhost:3000/' -I -s
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: ring-session=16bc73e9-ee70-4126-a7d4-c814fba7d6b7;Path=/;HttpOnly
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Content-Length: 28
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:21 GMT
> curl 'http://localhost:3000/' -s
Hello World with middleware!
> curl 'http://localhost:3000/testing' -I -s
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: ring-session=0e1792c2-de70-4405-984d-082f889886da;Path=/;HttpOnly
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Content-Length: 28
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:21 GMT
> curl 'http://localhost:3000/testing' -s
Hello World with middleware!
> curl 'http://localhost:3000/js/template.js' -I -s
HTTP/1.1 200 OK
Content-Length: 0
Last-Modified: Sun, 03 Apr 2016 04:47:10 GMT
Content-Type: text/javascript; charset=utf-8
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:21 GMT
> curl 'http://localhost:3000/js/template.js' -s
(function() {
var message = "I got loaded!";
document.addEventListener("DOMContentLoaded", function(event) {
if (console) {
console.log(message);
} else {
alert(message);
}
});
}());
> curl 'http://localhost:3000/css/template.css' -I -s
HTTP/1.1 200 OK
Content-Length: 0
Last-Modified: Sun, 03 Apr 2016 04:45:48 GMT
Content-Type: text/css; charset=utf-8
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:21 GMT
> curl 'http://localhost:3000/css/template.css' -s
body {
font-family: Helvetica;
}
Compojure itself just makes it easier for your application to handle the in-between of ring and your code. It ties the whole together quite transparently and so you barely even notice you're getting a lot from it but it's still awesome.
(ns samples.with-compojure
(:gen-class)
(:require
[org.httpkit.server :refer [run-server]]
[compojure.core :refer [defroutes GET PUT POST DELETE ANY]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults api-defaults]]
[environ.core :refer [env]]))
(defroutes site-routes
(GET "/" []
{:status 200 :headers {"Content-Type" "text/html"} :body "Hello World with compojure!"}))
(def handler
(wrap-defaults site-routes site-defaults))
(defn -main [& [port]] ;; entry point, lein run will pick up and start from here
(let [p (Integer. (or port (env :port) 5000))]
(run-server handler {:port p})))
The results are now as follow:
> curl 'http://localhost:3000/' -I -s
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: ring-session=9aba80cc-501c-4cbb-9d77-d578ceed6289;Path=/;HttpOnly
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Content-Length: 0
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:32 GMT
> curl 'http://localhost:3000/' -s
Hello World with compojure!
> curl 'http://localhost:3000/testing' -I -s
HTTP/1.1 404 Not Found
Content-Length: 0
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:32 GMT
> curl 'http://localhost:3000/testing' -s
Compojure handles everything that is route matching and other path matching activities. Note that this example is very simple with a single path. Composure handles wildcards in paths, variable capturing, contexts and many other things that would make the if condition in that request handler function a lot more complicated. If you didn't have compojure you'd have to handle all of this by yourself:
(ns samples.without-compojure
(:gen-class)
(:require
[org.httpkit.server :refer [run-server]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults api-defaults]]
[ring.util.request :refer [path-info]]
[environ.core :refer [env]]))
(def handler
(fn [request]
(if (= "/" (path-info request))
{:status 200 :headers {"Content-Type" "text/html"} :body "Hello World without compojure!"}
{:status 404 :body ""})))
(def app
(wrap-defaults handler site-defaults))
(defn -main [& [port]] ;; entry point, lein run will pick up and start from here
(let [p (Integer. (or port (env :port) 5000))]
(run-server app {:port p})))
Which produces about the same thing except for the 404 headers:
> curl 'http://localhost:3000/' -I -s
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: ring-session=a76f0a3a-5f15-4600-9bb3-4470735f2626;Path=/;HttpOnly
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Content-Length: 30
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:42 GMT
> curl 'http://localhost:3000/' -s
Hello World without compojure!
> curl 'http://localhost:3000/testing' -I -s
HTTP/1.1 404 Not Found
Set-Cookie: ring-session=205fcee8-160b-477f-9f00-26ac9af29ec4;Path=/;HttpOnly
Content-Type: application/octet-stream
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Content-Length: 0
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:43 GMT
> curl 'http://localhost:3000/testing' -s
When it comes to generating html structure, I've chosen hiccup and have been quite pleased as well. Somehow sequences (think of them as arrays if that makes it easier) with the first element as a tag and the rest as content works very well with html. A nice little map as an optional second parameter makes attributes pretty obvious and a couple of nice tricks allow hiccup to feel very much like css selectors. Easy and natural:
(ns samples.with-hiccup
(:gen-class)
(:use
[hiccup.page :only (html5 include-css include-js)])
(:require
[org.httpkit.server :refer [run-server]]
[compojure.core :refer [defroutes GET PUT POST DELETE ANY]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults api-defaults]]
[environ.core :refer [env]]))
(defroutes site-routes
(GET "/" request
(html5
[:head
[:title "Home"]
(include-css "/css/template.css")]
[:body
[:p "Hello World with hiccup"]
(include-js "/js/template.js")])))
(def handler
(wrap-defaults site-routes site-defaults))
(defn -main [& [port]] ;; entry point, lein run will pick up and start from here
(let [p (Integer. (or port (env :port) 5000))]
(run-server handler {:port p})))
With the results:
> curl 'http://localhost:3000/' -I -s
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: ring-session=f39247fe-c5ad-4b2b-9f07-039509b0c30b;Path=/;HttpOnly
X-Xss-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Content-Length: 0
Server: http-kit
Date: Sun, 03 Apr 2016 07:42:53 GMT
> curl 'http://localhost:3000/' -s
<!DOCTYPE html>
<html><head><title>Home</title><link href="/css/template.css" rel="stylesheet" type="text/css"></head><body><p>Hello World with hiccup</p><script src="/js/template.js" type="text/javascript"></script></body></html>
And since everything is just a sequence, it's super easy to just extract functions and share common structures between pages or within pages. I thought mixing clojure code and html would get real weird like it did in Smalltalk but somehow it worked a lot better.
Add to that the fact that Clojure runs on the JVM and it means that I could work with all of that while still being usable in a traditional enterprisy "give me a war and I'll deploy it". Gradle and lein don't quite interact together and you still have to maintain project.clj and build.gradle separately. It's not very hard to generate build.gradle from project.clj but you have to keep a pre-commit hook or some watcher to upgrade it when you change project.clj.
Overall, I would say I now have to think about where I want a project to go to chose between rails and clojure. Rails will give me more features faster: devise for authentication, cancancan for authorization, sprockets for asset-pipelines, lots of gem from aws-sdk to doorkeeper, sass-rails, will_paginate, new_relic and a bunch more. But clojure gives me access to all JVM. Sometimes with a bit more pain (need to handle the bridge or use the Java code) and sometimes with less than great bridges. But, overall, you can pull it together and, if you really need to, you can fall back to existing Java code or even flip to developing it if it makes more sense. You also get all the nice JIT compilation improvements the JVM can give. JRuby doesn't quite give you the same value and experience so, I'd pick clojure over ruby on any JVM environment and if the team I'm with if comfortable with functional programming.