Draft: First four chapters of Compojure tutorial

1,515 views
Skip to first unread message

James Reeves

unread,
Apr 25, 2009, 6:32:13 PM4/25/09
to Compojure
Foreword
========

This is a very rough draft of the tutorial I'm going to put on
compojure.org. It's not complete, but it covers most of the basics.
There's a possibility some of the terminology (such as handlers and
routes) might change, but I'll let you know if it does. The technical
content, however, should be accurate and up to date.

Criticism is very welcome; I'd like to know if anything is unclear or
could be better worded, or if I'm missing out anything.


Compojure from the bottom up
============================

1. Handlers

In Compojure, HTTP requests and HTTP responses are represented by
Clojure maps. A *handler* is a function that takes a request map as an
argument, and returns a response map.

{ request } --> handler --> { response }

A response map consists of three keys:

:status (Required, Integer)
The HTTP status code.

:headers (Required, Map)
A map of HTTP header names to header values.

:body (Optional, {String, ISeq, File, InputStream})
An object to be encoded as the HTTP response body.

A request map consists of many more keys. The most significant ones
are:

:request-method (Keyword)
The HTTP request method. Either :get, :head, :options, :put, :post
or :delete.

:uri (String)
The relative URI of the HTTP request.

See the request map documentation for more standard keys.

A handler processes the request map and returns a response map. The
simplest possible handler is one that always returns the same
response:

(defn hello-world [request]
{:status 200
:headers {}
:body "Hello World"})

Compojure provides the inline function `servlet` to convert a handler
function into a HttpServlet proxy compatible with many Java web
servers.

Here is an example of the a handler being turned into a servlet and
passed to an embedded web server:

(run-server {:port 8080}
"/*" (servlet hello-world))

By combining a handler with the `run-server` function, a basic web
application can be constructed:

(ns example-app
(:use compojure.server.jetty))

(defn hello-world [request]
{:status 200
:headers {}
:body "Hello World"})

(run-server {:port 8080}
"/*" (servlet hello-world))

If you run this code, you should be able to access a web page at:
http://localhost:8080


2. Middleware

*Middleware* are functions that take a handler as its first argument,
and returns a new handler function based on the original.

handler & args --> middleware --> handler

An example of a simple middleware function is one that adds a header
to the output of a handler:

(defn with-header [handler header value]
(fn [request]
(let [response (handler request)]
(assoc-in response [:headers header] value))))

To apply this to the existing `hello-world` handler, you can redefine
`hello-world` with the middleware wrapper.

(def hello-world
(-> hello-world
(with-header "X-Lang" "Clojure")
(with-header "X-Framework" "Compojure")))

But a more idiomatic way is to use the `decorate` macro:

(decorate hello-world
(with-header "X-Lang" "Clojure")
(with-header "X-Framework" "Compojure"))

The decorate macro produces the same effect, but retains the original
metadata of `hello-world`.

A number of middleware functions are included in Compojure. These
augment handlers in various ways. You can wrap a handler in many
middleware functions, or none at all. Some of the most commonly used
middleware functions are:

- with-params
- with-cookies
- with-multipart
- with-session


3. Routes

3.1. Route syntax

A *route* is a type of handler that returns nil if the request does
not match certain criteria. A route can be written:

(defn index-route [request]
(if (and (= (:request-method request) :get)
(= (:uri request) "/"))
{:status 200
:headers {}
:body "The index page"))

But as this is a very common task, Compojure provides macros that
remove the need for such verbose boilerplate. The idiomatic way of
writing the above route in Compojure is:

(def index-route
(GET "/" "The index page"))

The Compojure route syntax is very powerful, but is based on a few
basic principles.

3.1.1. The method macro

The first symbol is the *method macro* that denotes the HTTP request
method. In the above example, this is the GET macro. There are also
macros for all the other common HTTP methods:

GET, POST, PUT, DELETE and HEAD

Because sometimes you don't care what method is being used, there is
also:

ANY

Which matches any method.


3.1.2. The path template

The second item in the route form is the *path template*. This matches
against the HTTP request URI. The path template can include
parameters, which are identifiers denoted by a beginning ":":

(GET "/product/:id" ...)

A parameter will match a string of any character apart from "/", ".",
"," ";" and "?". The matched value is stored in a map the :route-
params key in the request map:

(GET "/product/:id"
(str "You chose product: "
(-> request :route-params :id)))

You can include more than one parameter, and even the same parameter
multiple times. In the latter case the value in the route-params map
will be a vector with all the matching values from the URI.

As well as parameters, you can match wildcards, denoted by a "*". A
wildcard will match a string of any character. The value matched by
the wildcard is stored under the :* key.

(GET "/public/*"
(str "Loading file: "
(-> request :route-params :*)))

As well as relative URIs, absolute URLs can also be matched:

(GET "http://www.example.com/" ...)

This behaviour is triggered when the beginning of the path template is
a URL scheme, such as "http://" or "https://". You can use parameters
or wildcards in the domain:

(GET "http://:subdomain.example.com/" ...)

But you cannot use a parameter to match the scheme. However, the
request map does contain the :scheme key for circumstances where it is
required to place the URL scheme into a variable.

For more precise control over URI matching, the path template can be
specified using a regular expression:

(GET #"/product/(\d+)" ...)

In this case the :route-params key contains a vector corresponding to
the groups matched by the expression.

(GET #"/product/(\d+)"
(str "You chose product: "
((:route-params request) 0)))

Unlike re-groups, the first element of the parameter vector is not the
entire match, but the first nested group.


3.1.3. The return value

In the Compojure route syntax, the return value represents a
modification to a blank response map:

{:status 200, :headers {}}

The class of the return value determines how it alters the response
map. The following classes are used:

java.lang.Integer
An integer return value sets the status code of the response

java.lang.String
A string return value is added to the response body

clojure.lang.ISeq
A return value of a Clojure sequence sets the response body

java.io.File
A return value of a File sets the response body

java.io.InputStream
A return value of an InputStream sets the response body

java.net.URL
A InputStream to the URL is opened and the response body set to the
stream

clojure.lang.Keyword
If the keyword is :next, the response is nil. Otherwise the keyword
is treated as a string.

java.util.Map
The map is intelligently merged into the response map

clojure.lang.Fn
The request and response maps are passed to the function as
arguments, and the return value of the function is used to determine
the response.

clojure.lang.IPersistentVector
Each element in the vector is used to update the response

Some examples of usage follow:

(GET "/"
"Index page")

(ANY "*"
[404 "Page Not Found"])

(GET "/image"
(File. "./public/image.png"))

(GET "/new-product"
(if product-released?
"Our product is amazing"
:next))

(GET "/map-example"
{:body "Hello World"})


3.1.4. Local bindings

The final useful piece of functionality the route syntax provides is a
small set of useful local bindings:

- params => (:params request)
- cookies => (:cookies request)
- session => (:session request)
- flash => (:flash request)

The :params key and the associated params binding provides a merged
map of all parameters from the request. This includes the contents
of :route-params (when a map), and the parameters added by the with-
params and with-multipart middleware.

Thus, an idiomatic and concise way of refering to route params is:

(GET "/product/:id"
(str "You chose product: " (params :id)))


3.2. Combining routes

Routes can be combined with the `routes*` function:

(def main-routes
(routes*
(GET "/"
"Index page")
(ANY "*"
[404 "Page Not Found"])))

The `routes*` function returns a new route. When supplied with a
request map, this new route tries each sub-route in turn until it
receieves a response that is not nil. The code for this is simple:

(defn routes* [& sub-routes]
(fn [request]
(some #(% request) sub-routes)))

The `routes*` function is the more primitive ancestor of the more
commonly used `routes` function. The difference between the two is
that `routes` adds two pieces of common middleware:

(defn routes [& sub-routes]
(-> (apply routes* sub-routes)
with-params
with-cookies))

It is recommended that `routes` be preferred for normal use.

For convenience, Compojure also provides a `defroutes` macro:

(defroutes main-routes
(GET "/"
"Index page")
(ANY "*"
[404 "Page not found"]))


4. HTML

4.1. Syntax

Compojure uses a syntax made up of vectors, maps and strings to
represent HTML. The `html` function translates this syntax into a
string of HTML.

Here is an example of the syntax:

[:h1 {:id "title"} "Hello World"]

In Compojure, this is referred to as a tag vector, so called because
it represents a HTML tag.

The first element in the vector is the tag name. This can be a
keyword, a string, or a symbol.

The second element can optionally be a map. If it is a map, it is
considered to represent the attributes of the tag, otherwise it is
treated as the tag's content.

Any further elements are treated as the content of the tag. A tag's
content can be made up of any number of strings or nested tag vectors.
Here are some examples:

[:div "Hello" "World"]

[:div [:div {:class "inner"} "Nested"]]

[:div [:span "Hello"] [:span "World"]]

A Clojure sequence is also considered valid content. Sequences are
automatically expanded out, such that this:

[:div (list "Hello" "World")]

Is considered equivalent to:

[:div "Hello" "World"]

This functionality is useful for functions that have a rest-param:

(defn html-document [title & body]
(html
[:html
[:head
[:title title]]
[:body
body]]))

Compojure also provides a shorthand for defining elements with id or
class attributes, based on standard CSS syntax. Any alphanumeric, "-"
or "_" after a "#" in the tag name is used as the id attribute:

[:h1#title "Compojure"]

Similarly, any alphanumeric, "-" or "_" after a "." is used as the
class attribute:

[:div.summary "A Clojure web framework"]

You can define many classes, but only one id using this syntax.

[:pre#example1.source.clojure
"(some example code)"]


- James

Adrian Cuthbertson

unread,
Apr 25, 2009, 11:44:49 PM4/25/09
to comp...@googlegroups.com
Excellent James, that really covers all the tricky bits.

Robert Campbell

unread,
Apr 26, 2009, 3:38:47 PM4/26/09
to comp...@googlegroups.com
In part 1.Handlers you give this example:

(ns example-app
(:use compojure.server.jetty))

(defn hello-world [request]
{:status 200
:headers {}
:body "Hello World"})

(run-server {:port 8080}
"/*" (servlet hello-world))


I believe you need to change it to:

(ns example-app
(:use compojure.http.servlet)
(:use compojure.server.jetty))

since you make use of (servlet)

James Reeves

unread,
Apr 26, 2009, 5:40:24 PM4/26/09
to Compojure
On Apr 26, 8:38 pm, Robert Campbell <rrc...@gmail.com> wrote:
> In part 1.Handlers you give this example:
>
>  (ns example-app
>    (:use compojure.server.jetty))
>
>  (defn hello-world [request]
>    {:status  200
>     :headers {}
>     :body    "Hello World"})
>
>  (run-server {:port 8080}
>    "/*" (servlet hello-world))
>
> I believe you need to change it to:
>
> (ns example-app
>   (:use compojure.http.servlet)
>   (:use compojure.server.jetty))
>
> since you make use of (servlet)

You're right - good catch.

- James

Robert Campbell

unread,
Apr 27, 2009, 7:37:55 AM4/27/09
to comp...@googlegroups.com
Thanks James, that worked perfectly. Just in case anyone else is
trying something similar, here's some code that will give you the full
domain name:

(defn assemble-domain [route-params]
(str (:domain-name route-params) "." (:top-level-domain route-params)))

(defroutes domain-routes
(ANY "http://*.:domain-name.:top-level-domain/*"
(str "Hello " (assemble-domain (request :route-params)))))

(defserver orchid-server
{:port 80}
"/*" (servlet domain-routes))

James Reeves

unread,
Apr 27, 2009, 2:46:00 PM4/27/09
to Compojure
On Apr 27, 12:37 pm, Robert Campbell <rrc...@gmail.com> wrote:
> Thanks James, that worked perfectly. Just in case anyone else is
> trying something similar, here's some code that will give you the full
> domain name

It's worth noting that the full domain is also stored in the "Host"
header of the HTTP request.

- James
Reply all
Reply to author
Forward
0 new messages