Programming for the web in Clojure isn't hard, but with layers of abstraction you can easily lose track of what is going on. In this talk, we'll dig deep into Ring, the request/response library that most Clojure web programming is based on. We'll see exactly what a Ring handler is and look at the middleware abstraction in depth. We'll then take this knowledge and deconstruct the Compojure routing framework to understand precisely how your web application responds to request. At the end of the talk you should thoroughly understand everything that happens in the request/response stack and be able to customize your web application stack with confidence.
Updated for Houston Clojure Meetup 2/28/14
12. (defn handler-status [req]
{:status 402
:headers {"Location"
"bitcoin:1G9TyAaKrfJn7q4Vrr15DscLXFSRPxBFaH?amount=.001"}})
Handlers can return status code and headers
13. ring.util.response/*
A few response helpers
(defn response
"Returns a skeletal Ring response with the given body, status of 200, and no
headers."
[body]
{:status 200
:headers {}
:body
body})
!
(defn not-found
"Returns a 404 'not found' response."
[body]
{:status 404
:headers {}
:body
body})
!
(defn redirect
"Returns a Ring response for an HTTP 302 redirect."
[url]
{:status 302
:headers {"Location" url}
:body
""})
20. ring.middleware.reload/wrap-reload
(defn wrap-reload
"Reload namespaces of modified files before the request is passed to the
supplied handler.
!
Takes the following options:
:dirs - A list of directories that contain the source files.
Defaults to ["src"]."
[handler & [options]]
(let [source-dirs (:dirs options ["src"])
modified-namespaces (ns-tracker source-dirs)]
(fn [request]
Smarter reloading surrounding the wrapped handler
(doseq [ns-sym (modified-namespaces)]
(require ns-sym :reload))
(handler request))))
21. ring.server.standalone/add-middleware
This is what “lein ring server” does
(defn- add-middleware [handler options]
(-> handler
(add-auto-refresh options)
(add-auto-reload options)
(add-stacktraces options)))
25. compojure.handler/api
An existing minimal stack for APIs
(defn api
"Create a handler suitable for a web API. This adds the following
middleware to your routes:
- wrap-params
- wrap-nested-params
- wrap-keyword-params"
[routes]
(-> routes
wrap-keyword-params
wrap-nested-params
wrap-params))
26. compojure.handler/site
(defn site
"Create a handler suitable for a standard website. This adds the
following middleware to your routes:
- wrap-session
- wrap-flash
- wrap-cookies
- wrap-multipart-params
- wrap-params
- wrap-nested-params
- wrap-keyword-params
!
A map of options may also be provided. These keys are provided:
:session
- a map of session middleware options
:multipart - a map of multipart-params middleware options"
[routes & [opts]]
(-> (api routes)
(with-opts wrap-multipart-params (:multipart opts))
Extends the API stack
(wrap-flash)
(with-opts wrap-session (:session opts))))
27. Use it, or make your own
(def handler (-> #'app
compojure.handler/site
ring-stack))
30. Not ring handlers
because they don’t
take a request.
(defn home []
(response/response "Home Page"))
(defn foo []
(response/response "Foo Page"))
(defn foo-n [n]
(response/response (str "This is Foo#" n)))
(defn app1 [req]
(condp re-matches (:uri req)
#"/"
(home)
#"/foo"
(foo)
#"/foo/(.*)" :>> #(foo-n (second %))
(response/not-found "Wat")))
Select the page to
show based on URL
32. Include the pattern
(defn my-route [pattern page-fn]
(fn [req]
(if-let [match (re-matches pattern (:uri req))]
((route-to handler) page-fn))))
(defn app3 [req]
(let [my-routes [(my-route #"/"
home)
Much cleaner
(my-route #"/foo"
foo)
(my-route #"/foo/(.*)" foo-n)
(my-route #".*"
#(response/not-found "Wat"))]]
(some #(% req) my-routes)))
The first route that responds wins
33. Routing fn includes method and path
(defn app4 [req]
(let [my-routes [(GET "/" [] (home))
Some extra macro magic
(GET "/foo" [] (foo))
(GET "/foo/:id" [id] (foo-n id))
(route/not-found "Wat")]]
(some #(% req) my-routes)))
Some things never change
34. compojure.core/*
(defn make-route
"Returns a function that will only call the handler if the method and Clout
route match the request."
[method route handler]
(if-method method
(if-route route
(fn [request]
(render (handler request) request)))))
!
(defn- compile-route
"Compile a route in the form (method path & body) into a function."
[method route bindings body]
`(make-route
~method ~(prepare-route route)
(fn [request#]
(let-request [~bindings request#] ~@body))))
!
(defmacro GET "Generate a GET route."
[path args & body]
(compile-route :get path args body))
36. (defn routing
"Apply a list of routes to a Ring request map."
[request & handlers]
(some #(% request) handlers))
!
(defn routes
"Create a Ring handler by combining several handlers into one."
[& handlers]
#(apply routing % handlers))
39. (defmacro context
"Give all routes in the form a common path prefix and set of bindings.
!
The following example demonstrates defining two routes with a common
path prefix ('/user/:id') and a common binding ('id'):
!
(context "/user/:id" [id]
(GET "/profile" [] ...)
(GET "/settings" [] ...))"
[path args & routes]
`(#'if-route ~(context-route path)
(#'wrap-context
(fn [request#]
(let-request [~args request#]
(routing request# ~@routes))))))
40. compojure.response/Renderable
(defprotocol Renderable
(render [this request]
"Render the object into a form suitable for the given request map."))
!
(extend-protocol Renderable
nil …
String …
APersistentMap …
IFn …
IDeref …
File …
ISeq …
InputStream …
URL … )
Generate response maps based on types