Last active
October 23, 2024 07:41
-
-
Save saiberz/5a1056a1b5bcc088c97c to your computer and use it in GitHub Desktop.
Introduction to Compojure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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