Idiomatic usage of Compojure using sessionless cookies.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
src rm 0 Nov 9, 2011
.gitignore reference links work? Nov 7, 2011 update cookie pix Nov 20, 2012
project.clj update Nov 8, 2011

Compojure Cookies Example 2011


Clojure, being a relatively new language, uses an even newer web framework: Compojure.

Compojure, still sporting a sub 1.0 version, being under active development and reduced to a thin veneer over Ring may prove challenging for developers. If for any reason because many examples and tutorials are just outdated.

This demonstrates the use of session-less cookies in Compojure, with working examples.

The Bare Essentials

Example 1, a simple hello-world application is suitable for running on Heroku.

(ns example1
  (:use [ring.adapter.jetty :only [run-jetty]]
        [compojure.core     :only [defroutes GET]]))

(defroutes routes
  (GET  "/" [] "Hi there"))

(defn -main []
  (run-jetty routes {:port (if (nil? (System/getenv "PORT")) 
                             8000 ; localhost or heroku?
                             (Integer/parseInt (System/getenv "PORT")))}) )

After you check out the project from GitHub, it's easy to see in action:

$ lein run -m example1

Middleware is Features

Example 2, the addition of a very simple form requires some changes.

(ns example2
  (:use [ring.adapter.jetty             :only [run-jetty]]
        [compojure.core                 :only [defroutes GET POST]]
        [ring.middleware.params         :only [wrap-params]]))

(defroutes routes
  (POST "/" [name] (str "Thanks " name))
  (GET  "/" [] "<form method='post' action='/'> What's your name? <input type='text' name='name' /><input type='submit' /></form>"))

(def app (wrap-params routes))

(defn -main []
  (run-jetty app {:port (if (nil? (System/getenv "PORT")) 
                          8000 ; localhost or heroku?
                          (Integer/parseInt (System/getenv "PORT")))}) )

The new POST route uses the name variable from the form. This is possible because we're now leveraging middleware:

(def app (wrap-params routes))

Simply put: middleware is features. Rather than forcing you into a one-size-fits all model, it's a way to mix and match whichever ones you need.

In this case, we have to process form variables. wrap-params is what does this for us by making the form variable name available as a local.

You can also look at from an efficiency point of view: ALL we're doing is accessing the form parameters. We aren't using sessions or a plethora of other features that we may or may not want.

C is for Clojure

Example3, cookie stuffing without sessions.

(ns example3
  (:use [ring.adapter.jetty             :only [run-jetty]]
        [compojure.core                 :only [defroutes GET POST]]
        [ring.middleware.cookies        :only [wrap-cookies]]
        [ring.middleware.params         :only [wrap-params]]
        [ring.middleware.keyword-params :only [wrap-keyword-params]]))

(defn main-page [cookies]
  (str "Hi there "
       (if (empty? (:value (cookies "name")))
         "<form method='post' action='/'> What's your name? <input type='text' name='name' /><input type='submit' /></form>"
         (:value (cookies "name")))))

(defn process-form [params cookies]
  (let [name (if (not (empty? (:name params)))
               (:name params)
               (:value (cookies "name")))]

    ;; set cookie, return html
    {:cookies {"name" name}
     :body (str "<html><head><meta HTTP-EQUIV='REFRESH' content='5; url='/'\"</head><body>Thanks!</body></html>")}))
(defroutes routes
  (POST "/" {params :params cookies :cookies} (process-form params cookies))
  (GET  "/" {cookies :cookies}                (main-page cookies)))

(def app (-> #'routes wrap-cookies wrap-keyword-params wrap-params))

(defn -main []
  (run-jetty app {:port (if (nil? (System/getenv "PORT")) 
                          8000 ; localhost or heroku?
                          (Integer/parseInt (System/getenv "PORT")))}) )

Keyword Params

Starting from the bottom, we're now wrapping cookies and keyword params using threading:

(def app (-> #'routes wrap-cookies wrap-keyword-params wrap-params))

Keyword params change the input parameters from string-based to keyword based. By default it's string based because form variables can contain spaces, however I've yet to see it in real life.

{"name" "Hello world"} ; params without keyword-params
{:name  "Hello world"} ; params with keyword-params

Destructuring Syntax

The routes have a startling new destructing syntax:

(POST "/" {params :params cookies :cookies} (process-form params cookies))

Because we want to manipulate cookies, we need to operate at slightly lower level of abstraction. This syntax allow us to pass in cookies so they can be read.

Return Value

{:cookies {"name" name}
 :body (str "<html><head> ...

The return value isn't just a string anymore, in order to save a cookie in the browser we need to pass them out much in the manner it arrived. Besides, returning a string is a shortcut for setting the :body, you also can set other various things like headers.

That's it

This avoids the overhead and complexity of sessions, meaning the cookie is stored unencrypted and in plain view in the user's browser (for obvious reasons, avoid this technique for sensitive data). Unlike sessions which store a token that is associated with your data in the web-server, the browser holds everything so the data survives server restarts but is readable and modifiable by the user.

It's not a lot of code, you've learned alternative input and outputs, destructuring, form processing, Ring's middleware and cookie management.

Now grab the code from GitHub and make something fun:

git clone