Clojure Web Development Essentials - Sample Chapter
Clojure Web Development Essentials - Sample Chapter
P U B L I S H I N G
E x p e r i e n c e
D i s t i l l e d
$ 44.99 US
27.99 UK
C o m m u n i t y
Ryan Baldwin
Develop your own web application with the effective use of the
Clojure programming language
Ryan Baldwin
In this chapter, we'll create a new web application called hipstr, an application
that will help us track our vinyl collection and endow us with obscure credibility.
We'll build this application with each subsequent chapter by creating our own route
handlers, interacting with a database, authenticating users, validating form input,
and reading/writing cookies. By the end of this book, we'll know the Clojure web
basics well enough that we'll be wearing plaid shirts and sipping bourbon aged in
casks from a place nobody's ever heard of.
Leiningen
Our project will rely heavily on Leiningen, a build and task tool for Clojure.
Leiningen allows us to easily maintain our application's dependencies, assists us
in common tasks such as database migrations, running tests, producing binaries
(jars and wars), and a plethora of other things. Leiningen is akin to Java's build
tool Maven (http://maven.apache.org), and Ruby's Rake (http://github.com/
jimweirich/rake). As Leiningen's web page (http://leiningen.org) concisely
puts it: for automating Clojure projects without setting your hair on fire.
Using Leiningen
The basic makeup of a Leiningen task can be summarized as follows:
# lein $TASK $TASK_ARGUMENTS
You can also view the help content for a specific Leiningen task by executing the
following command:
# lein help $TASK
You can use these commands whenever you need to know how to do something
in Leiningen.
[8]
Chapter 1
The new task expects, at a minimum, a name for the project ($PROJECT_NAME).
Optionally, we can provide a specific template to use ($TEMPLATE_NAME). If we
don't specify a template, then lein will use the default template, which is a general
template for developing libraries.
For our project we'll use the Luminus template, an excellent template for web
applications. Luminus generates a project and wires in the libraries to support pretty
much every aspect of web development including sessions, cookies, route handling,
and template rendering.
At the time of this writing, the Luminus template was at version
1.16.7. To ensure the code examples in this book work, you can
force Leiningen to use a specific version of Luminus by modifying
Leiningen's profiles.clj file (typically found in your home
directory, in a folder called .lein) to include the specific version
of Luminus. For example:
:user {:plugins [[luminus/lein-template "1.16.7"]]}
[9]
In the preceding command line, the lein ring server command updates our class
path with the dependencies required to compile and run the app. It then launches
the development server (an embedded Jetty server) and starts serving on port 3000.
Lastly, it launches our default web browser and navigates to the root page.
In the preceding example, ring is the Leiningen task, and server
is the ring subtask. You can view a full list of ring subtasks by
entering the lein help ring command in your terminal.
The subsequent output of lein ring server is a series of debug statements that
lets us know what the heck is going on during the startup process. Any generated
exceptions or problems that occur while attempting to launch the application will
be emitted as part of this output.
Getting help
If anything doesn't go as planned, or you're stumped and confused, feel free to
check the Luminus documentation at http://www.luminusweb.net. You can
also get some help from people in the Luminus community (https://groups.
google.com/forum/?fromgroups#!forum/luminusweb) or the Ring community
(https://groups.google.com/forum/?fromgroups#!forum/ring-clojure).
Of course, there's always the Clojure group on Google Groups (https://groups.
google.com/forum/#!forum/clojure).
[ 10 ]
Chapter 1
:exclusions [com.keminglabs/cljx]]
[environ "1.0.0"]
[im.chit/cronj "1.4.2"]
[noir-exception "0.2.2"]
[prone "0.6.0"]]
The first dependency should look familiar (if not, then this book isn't for you yet).
The rest, however, might appear to be a mystery. I'll spare you the effort of searching
it online and break it down for you.
weavejester/ring-server
timbre: Timbre is a pure Clojure logging library. It's pretty much like every
other logging library on the planet, complete with somewhat confusing
configuration. We'll cover Logging in Chapter 3, Logging. You can also visit
https://github.com/ptaoussanis/timbre, to get more information
on Timbre.
[ 11 ]
tower: This is similar to its sibling timbre, and is a pure Clojure library that
https://github.com/ptaoussanis/tower.
prone: This produces the most amazing exception reporting output you
might have ever seen. (https://github.com/magnars/prone).
[ 12 ]
Chapter 1
[ 13 ]
The src directory contains all of the necessary namespaces for running our
application, and the test directory contains all the necessary namespaces for
testing our src.
In addition to the directories, however, Luminus also generated some files in the
src directory. These files are the bare minimum requirement to successfully run
our application, and each one handles specific functionality. Let's take a brief look
at the base functionality contained in each file.
util.clj
The hipstr.util namespace is a simple namespace where you can put various
helper functions you find yourself frequently using during the development of your
application. Out of the box, Luminus generates a hipstr.util namespace with a
single function, md->html, which converts markdown into HTML. Typically, I try
to avoid namespaces such as util.clj because they eventually turn into the junk
drawer in your kitchen, but they can be useful on smaller projects if things don't get
too crowded. The following block of code shows the hipstr.util namespace:
(ns hipstr.util
(:require [noir.io :as io]
[markdown.core :as md]))
(defn md->html
"reads a markdown file from public/md and returns an HTML
string"
[filename]
(md/md-to-html-string (io/slurp-resource filename)))
session_manager.clj
One of lib-noir's exposed functionalities is session management (which we'll discuss
in detail in Chapter 10, Sessions and Cookies). The default session pool in Luminus is
an in-memory session pool, a shortcoming of which is that expired sessions are only
removed from memory when the server handles a request associated with an expired
session. As a result, old stale sessions can linger in memory indefinitely, straining
memory resources on the server. Luminus boilerplates a cronj job in the hipstr.
sessions-manager namespace, which occasionally removes stale, unused sessions.
By default, the job runs every 30 minutes. Take a look at the following lines of code:
(ns hipstr.session-manager
(:require [noir.session :refer [clear-expired-sessions]]
[cronj.core :refer [cronj]]))
[ 14 ]
Chapter 1
(def cleanup-job
(cronj
:entries
[{:id "session-cleanup"
:handler (fn [_ _] (clear-expired-sessions))
:schedule "* /30 * * * * *"
:opts {}}]))
layout.clj
The hipstr.layout namespace houses the functions that are used to render the
HTTP response body. By default, Luminus creates a single function, render, which
will render any Selmer template onto the HTTP response.The following lines of code
is for the hipstr.layout namespace:
(ns hipstr.layout
(:require [selmer.parser :as parser]
[clojure.string :as s]
[ring.util.response :refer [content-type response]]
[compojure.response :refer [Renderable]]
[environ.core :refer [env]]))
(def template-path "templates/")
(deftype RenderableTemplate [template params]
Renderable
(render [this request]
(content-type
(->> (assoc params
(keyword
(s/replace template #".html" "-selected"))"active"
:dev (env :dev)
:servlet-context
(if-let [context (:servlet-context request)]
;; If we're not inside a serlvet environment
;; (for example when using mock requests), then
;; .getContextPath might not exist
(try (.getContextPath context)
(catch IllegalArgumentException _
context))))
(parser/render-file (str template-path template))
response)
[ 15 ]
The key to the hipstr.layout namespace is that it remains high level and generic.
You should avoid writing functions with domain knowledge in this namespace, and
instead focus on generating response bodies. If you put an explicit URL or filename
in this namespace, you're probably doing it wrong.
middleware.clj
Middleware, for the unfamiliar, is a function that can work with an incoming request
prior to the request being handled by the main application (that is our proverbial
business logic). Its function is similar to how a car moves through an assembly line; each
employee working the line is responsible for interacting with the car in some specific
way. Much like how at the end of the assembly line the car is in its final state and ready
for consumption, so is the request in its final state and ready for processing by the
main application. The following code is for the hipstr.middleware namespace:
(ns hipstr.middleware
(:require [taoensso.timbre :as timbre]
[selmer.parser :as parser]
[environ.core :refer [env]]
[selmer.middleware :refer [wrap-error-page]]
[prone.middleware :refer [wrap-exceptions]]
[noir-exception.core :refer [wrap-internal-error]]))
(defn log-request [handler]
(fn [req]
(timbre/debug req)
(handler req)))
(def development-middleware
[wrap-error-page
wrap-exceptions])
(def production-middleware
[#(wrap-internal-error % :log (fn [e] (timbre/error e)))])
(defn load-middleware []
(concat (when (env :dev) development-middleware)
production-middleware))
[ 16 ]
Chapter 1
routes/home.clj
One of the directories that Luminus generated was a route folder. Routes are what tie
a request to a specific handler (or, in layman's terms, a chunk of code to be executed
based on the URL the request is sent to). Luminus generates 2 routes for us:
A / route, which renders the result of calling the home-page function, which
ultimately renders the home page you see at startup
We will create a couple of our own routing namespaces over the course of this
book. The routes we'll create in those namespaces will follow the same pattern
demonstrated in the preceding hipster.routes.home namespace. We'll talk
a bit more about routes in Chapter 4, URL Routing and Template Rendering.
[ 17 ]
handler.clj
Everything we've seen in this chapter is brought together into a single, harmonious,
running application in the hipstr.handler namespace, explained in the following
lines of code. Opening the file for a cursory scan reveals our cron job to clean up
expired sessions, the home-routes from the hipstr.routes.home namespace,
the configuration of our Timbre logging, and so on.
(ns hipstr.handler
(:require [compojure.core :refer [defroutes]]
; ... snipped for brevity
[cronj.core :as cronj]))
(defroutes base-routes
(route/resources "/")
(route/not-found "Not Found"))
(defn init
"init will be called once when
app is deployed as a servlet on
an app server such as Tomcat
put any initialization code here"
[]
; snipped for brevity )
(defn destroy
"destroy will be called when your application
shuts down, put any clean up code here"
[]
; ... snipped for brevity ...)
;; timeout sessions after 30 minutes
(def session-defaults
{:timeout (* 60 30)
:timeout-response (redirect "/")})
(defn- mk-defaults
"set to true to enable XSS protection"
[xss-protection?]
;... snipped for brevity ...
)
(def app (app-handler
;; add your application routes here
[ 18 ]
Chapter 1
[home-routes base-routes]
;; add custom middleware here
:middleware (load-middleware)
:ring-defaults (mk-defaults false)
;; add access rules here
:access-rules []
;; serialize/deserialize the following data formats
;; available formats:
;; :json :json-kw :yaml :yaml-kw :edn :yaml-in-html
:formats [:json-kw :edn :transit-json]))
We'll get into detail about what all is happening, and when, in Chapter 2, Ring and the
Ring Server.
repl.clj
The last Luminus generated namespace, hipstr.repl, is one that often confuses
beginners because it's strikingly similar to hipster.handler. The hipstr.repl
namespace has a start-server and stop-server function, much like hipster.
handler. However, hipstr.repl allows us to start and stop our development server
from the Clojure REPL. This might seem like a weird thing to do, but by running
our server from the REPL we can modify our running system and the changes will
be "automagically" reloaded in our server. No need for the time consuming and
frustrating "compile-deploy-restart-grab-a-coffee-and-twiddle-your-thumbs cycle!"
(ns hipstr.repl
(:use hipstr.handler
ring.server.standalone
[ring.middleware file-info file]))
(defonce server (atom nil))
(defn get-handler []
;; #'app expands to (var app) so that when we reload our code,
;; the server is forced to re-resolve the symbol in the var
;; rather than having its own copy. When the root binding
;; changes, the server picks it up without having to restart.
; ... snipped for brevity ...
)
(defn start-server
"used for starting the server in development mode from REPL"
[& [port]]
[ 19 ]
Incorporating the REPL into your development workflow is a wonderful thing to do.
You can load your namespace into the REPL while you work on it and test the code
while you're developing right then and there. In fact, some IDEs such as LightTable
take this a step further, and will "live-evaluate" your code as you type. The ability of
running the dev server from the REPL completes the circle.
If you're not currently using a decent IDE for Clojure development,
I strongly encourage you to give LightTable a try. It's free, open source,
lightweight, and very different than anything you're used to. It's quite
good. Check it out at http://www.lighttable.com.
Summary
In this chapter, you learned how to generate a new Clojure-based web application
using Leiningen and the Luminus template. We also got a high-level understanding of
each dependency, and how Luminus structures its projects. In the next chapter we'll
take a detailed look at the Ring and Ring Server libraries, and what they're responsible
for. It sounds a little dry, I know, but I recommend that you read it. There will be cake
and punch at the end, but without all the calories of cake and punch.
[ 20 ]
www.PacktPub.com
Stay Connected: