Skip to content

Instantly share code, notes, and snippets.

@saiberz
Last active December 7, 2022 22:40
Show Gist options
  • Save saiberz/5a1056a1b5bcc088c97c to your computer and use it in GitHub Desktop.
Save saiberz/5a1056a1b5bcc088c97c to your computer and use it in GitHub Desktop.
Introduction 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)"]
- By James Reeves
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment