Middleware in Clojure
This work is licensed under a Creative Commons Attribution 3.0 Unported License
([Link] (including images & stylesheets). The source is available
on Github ([Link]
What is Middleware?
Middleware in Clojure is a common design pattern for threading a request through a series of functions
designed to operate on it as well as threading the response through the same series of functions.
Middleware is used in many Clojure projects such as Ring ([Link]
Compojure ([Link] reitit
([Link]
([Link] and Kit ([Link]
The client function
The base of all middleware in Clojure is the client function, which takes a request object (usually a
Clojure map) and returns a response object (also usually a Clojure map).
For example, let's use a client function that pulls some keys out of a map request and does an HTTP
GET on a site:
(ns [Link]
(:require [[Link] :as http]))
(defn client [request]
(http/get (:site request) (:options request)))
To use the client method, call it like so (response shortened to fit here):
(client {:site "[Link] :options {}})
;; ⇒ {:status 200, :headers {...}, :request-time 3057, :body "..."}
Now that a client function exists, middleware can be wrapped around it to change the request, the
response, or both.
Let's start with a middleware function that doesn't do anything. We'll call it the no-op middleware:
;; It is standard convention to name middleware wrap-<something>
(defn wrap-no-op
;; the wrapping function takes a client function to be used...
[client-fn]
;; ...and returns a function that takes a request...
(fn [request]
;; ...that calls the client function with the request
(client-fn request)))
So how is this middleware used? First, it must be 'wrapped' around the existing client function:
(def new-client (wrap-no-op client))
;; Now new-client can be used just like the client function:
(new-client {:site "[Link] :options {}})
;; ⇒ {:status 200, :headers {...}, :request-time 3057, :body "..."}
It works! Now it's not very exiting because it doesn't do anything yet, so let's add another middleware
wrapper that does something more exiting.
Let's add a middleware function that automatically changes all "HTTP" requests into "HTTPS" requests.
Again, we need a function that returns another function, so we can end up with a new method to call:
;; assume (require '[[Link] :as str])
(defn wrap-https
[client-fn]
(fn [request]
(let [site (:site request)
new-site (str/replace site "http:" "https:")
new-request (assoc request :site new-site)]
(client-fn new-request))))
This could be written more concisely using -> and update , if you prefer:
;; assume (require '[[Link] :as str])
(defn wrap-https
[client-fn]
(fn [request]
(-> request
(update :site #(str/replace % "http:" "https:"))
(client-fn))))
The wrap-https middleware can be tested again by creating a new client function:
(def https-client (wrap-https client))
;; Notice the :trace-redirects key shows that HTTPS was used instead
;; of HTTP
(https-client {:site "[Link] :options {}})
;; ⇒ {:trace-redirects ["[Link]
;; :status 200,
;; :headers {...},
;; :request-time 3057,
;; :body "..."}
Middleware can be tested independently of the client function by providing the identity function (or any
other function that returns a map). For example, we can see the wrap-https middleware returns the
clojure map with the :site changed from 'http' to 'https':
((wrap-https identity) {:site "[Link]
;; ⇒ {:site "[Link]
Combining middleware
In the previous example, we showed how to create and use middleware, but what about using multiple
middleware functions? Let's define one more middleware so we have a total of three to work with. Here's
the source for a middleware function that adds the current data to the response map:
(defn wrap-add-date
[client]
(fn [request]
(let [response (client request)]
(assoc response :date ([Link].)))))
And again, we can test it without using any other functions using identity as the client function:
((wrap-add-date identity) {})
;; ⇒ {:date #inst "2023-11-12T[Link].081-00:00"}
Middleware is useful on its own, but where it becomes truly more useful is in combining middleware
together. Here's what a new client function looks like combining all the middleware:
(def my-client (wrap-add-date (wrap-https (wrap-no-op client))))
(my-client {:site "[Link]
;; ⇒ {:date #inst "2023-11-12T[Link].616-00:00",
;; :cookies {...},
;; :trace-redirects ["[Link]
;; :request-time 1634,
;; :status 200,
;; :headers {...},
;; :body "..."}
(The response map has been edited to take less space where you see ... )
Here we can see that the wrap-https middleware has successfully turned the request for
[Link] into one for [Link] additionally the wrap-add-date middleware
has added the :date key with the date the request happened. (the wrap-no-op middleware did execute,
but since it didn't do anything, there's no output to tell)
This is a good start, but adding middleware can be expressed in a much cleaner and clearer way by using
Clojure's threading macro, -> . The my-client definition from above can be expressed like this:
(def my-client
(-> client
wrap-no-op
wrap-https
wrap-add-date))
(my-client {:site "[Link]
;; ⇒ {:date #inst "2023-11-12T[Link].389-00:00",
;; :cookies {...},
;; :trace-redirects ["[Link]
;; :request-time 1630,
;; :status 200,
;; :headers {...},
;; :body "..."}
Something else to keep in mind is that middleware expressed in this way will be executed from the bottom
up, so in this case, wrap-add-date will call wrap-https , which in turn calls wrap-no-op , which finally
calls the client function.
If you have a lot of middleware to combine, it can be easier to use reduce and a vector of middleware
functions. See how clj-http does this for its default stack of middleware:
(defn wrap-request
"Returns a batteries-included HTTP request function corresponding to the given
core client. See default-middleware for the middleware wrappers that are used
by default"
[request]
(reduce (fn wrap-request* [request middleware]
(middleware request))
request
default-middleware))
See clj-http 's [Link] ([Link]
http/blob/d92be158230e8094436f415324d96f2bd7cf95f7/src/clj_http/[Link]#L1125-L1166) for the full
source.
« Working with Files and Directories in Clojure (/articles/cookbooks/files_and_directories/) || Parsing XML
in Clojure » (/articles/cookbooks/parsing_xml_with_zippers/)
Links
About (/articles/about/)
Table of Contents (/articles/content/)
Getting Started (/articles/tutorials/getting_started/)
Introduction to Clojure (/articles/tutorials/introduction/)
Clojure Editors (/articles/tutorials/editors/)
Clojure Community (/articles/ecosystem/community/)
Basic Web Development (/articles/tutorials/basic_web_development/)
Language: Functions (/articles/language/functions/)
Language: [Link] (/articles/language/core_overview/)
Language: Collections and Sequences (/articles/language/collections_and_sequences/)
Language: Namespaces (/articles/language/namespaces/)
Language: Java Interop (/articles/language/interop/)
Language: Polymorphism (/articles/language/polymorphism/)
Language: Concurrency and Parallelism (/articles/language/concurrency_and_parallelism/)
Language: Macros (/articles/language/macros/)
Language: Laziness (/articles/language/laziness/)
Language: Glossary (/articles/language/glossary/)
Ecosystem: Library Development and Distribution (/articles/ecosystem/libraries_authoring/)
Ecosystem: Web Development (/articles/ecosystem/web_development/)
Ecosystem: Generating Documentation (/articles/ecosystem/generating_documentation/)
Building Projects: [Link] and the Clojure CLI (/articles/cookbooks/cli_build_projects/)
Data Structures (/articles/cookbooks/data_structures/)
Strings (/articles/cookbooks/strings/)
Mathematics with Clojure (/articles/cookbooks/math/)
Date and Time (/articles/cookbooks/date_and_time/)
Working with Files and Directories in Clojure (/articles/cookbooks/files_and_directories/)
Middleware in Clojure
Parsing XML in Clojure (/articles/cookbooks/parsing_xml_with_zippers/)
Growing a DSL with Clojure (/articles/cookbooks/growing_a_dsl_with_clojure/)
Copyright © 2024 Multiple Authors
Powered by Cryogen ([Link]