Moving from Noir to Compojure and lib-noir

I have a small web application I wrote a while back, using the Noir framework. As pointed out on the clj-noir group by Anthony Grimes, Noir’s maintainer, Noir is now deprecated. So I spent an hour or two doing the most trivial possible translation of my application from Noir to a Compojure + lib-noir based web stack. I’m documenting the process here in case someone else finds it helpful.

Updating project.clj

First, I made the following changes to myproject.clj file. I modified :dependencies as follows:

  1. Removed [noir "1.0.3-beta3"]
  2. Added [lib-noir "0.3.5"] and [compojure "1.1.5"]
  3. Added [ring-server "0.2.7"] (to assist in starting the server, since Noir used to do this for me)

I then added [lein-ring "0.8.2"] to my :plugins key, which adds common Ring tasks to Leiningen, such as running a development server with a simple lein ring server. This necessitated the addition of a :ring key:

:ring {:handler myproject/app}

containing a map that points to the Ring handler defining my application – more about this handler in a bit. There are some other useful things you can stick in the :ring key’s map; check out the lein-ring documentation for details.

I added some additional configuration information to my :profiles, which you can read more about in the Ring documentation.

{:production {:ring {:open-browser? false
                       :stacktraces? false
                       :auto-reload? false}}
   :dev {:dependencies [[ring-mock "0.1.3"]
                        [ring/ring-devel "1.1.8"]]}}

I also added :min-lein-version "2.0.0" to the project map.

I previously had added Hiccup to my dependencies; you will need to do so, too, if you haven’t already, if you’re following this document as a guide.

Converting server.clj

If you used lein new noir and/or followed the Noir tutorial, you likely have a server.clj file kicking around that used to be responsible for starting the web server and adding what Noir calls views to it. For my quick-and-dirty conversion, I chose to place my Compojure routes in this file, and to define the Ring handler there.

First, I removed noir.server from my (ns)form’s :require. I removed the -main function, as I am now going to use lein ring and related functionality to start the web server. I added [compojure.route :as route] and [noir.util.middleware :as nm] to my :require, for reasons that will become clear momentarily.

I created a var, app-routes to hold the sequence of routes that my application would handle, and initialized it with a simple catch-all to handle 404s:

(def app-routes
  [(route/not-found "Not Found")])

I also defined app as the Ring handler, using lib-noir’s noir.util.middleware/app-handler, which takes a sequence of routes and returns a handler wrapped in all of Noir’s default middleware. I discovered the hard way if you don’t do this or some variant of it, many of Noir’s functions, such as validation, don’t work. Duh!

(def app
  (nm/app-handler app-routes))

If you’d already converted to using lein-ring, as suggested by http://www.webnoir.org/tutorials/others/, you may not need to do some or all of these steps.

Converting the Views

Again, if you used lein new noir and/or the tutorial, your pages are defined by defpartials and defpages in files in the views/ directory.

In each such file, I removed noir.core from its (ns) form.

I handled defpartial by essentially macro-expanding it. defpartial is a convenience macro that returns a function that returns Hiccup-style HTML. I replaced it with a plain old defn that wraps its contents in (hiccup.core/html).

defpage is a slightly stickier wicket. It’s a stateful abstraction that chooses One Way to define routes and return functions that render html; as such, it’s very simple. If you’re defining very static, straightforward routes, it’s much less verbose than how I chose to unpack it. Nevertheless, here’s what I did:

  1. I replaced each defpage with a defn, where the function name was chosen to correspond to the URI. For example, (defpage "/" [] ... became (defn index-page [] ....

  2. I added a route to app-routes (above) to reflect this route, e.g.,: (GET "/" [] (the-relevant-view/index-page)) (where the-relevant-view is the namespace the newly-defined index-page function lives in).

  3. For defpages that used the {:as params} style of destructuring to get the parameter map (as suggested in the Noir forms tutorial), I converted (defpage [:post "/user/add"] {:as user} ... to something line (defn user-add [user] ...) with a (POST "/user/add" [:as {user :params}] (the-relevant-view/user-add user)) route.

  4. Finally, any uses of (render) were replaced with a direct call to the correct function to render that page.

Once this all was done, I could lein ring server and things just worked.

Marc Liberatore
Marc Liberatore
Senior Teaching Faculty

My research interests include anonymity systems, file and network forensics, and computer science pedagogy.