Learn how to write custom middleware in Clojure to handle cross-cutting concerns like logging, authentication, and request transformation.
Middleware in web development acts as a bridge between the HTTP request and the application logic. It is used to handle cross-cutting concerns such as logging, authentication, request throttling, and request/response transformation. In Clojure, middleware is a powerful concept that allows developers to compose web applications in a modular and reusable way. In this section, we will explore how to write custom middleware in Clojure, drawing parallels to Java concepts where applicable.
In Clojure, middleware is a higher-order function that takes a handler function and returns a new handler function. This new handler can modify the request before passing it to the original handler or modify the response before returning it to the client. This concept is similar to Java’s servlet filters, which intercept requests and responses to apply cross-cutting concerns.
A typical middleware function in Clojure looks like this:
(defn wrap-example-middleware [handler]
(fn [request]
;; Pre-processing the request
(let [modified-request (assoc request :example-key "example-value")]
;; Call the original handler with the modified request
(let [response (handler modified-request)]
;; Post-processing the response
(assoc response :example-header "example-value")))))
Let’s create a custom middleware that logs requests and responses. This middleware will demonstrate how to intercept and log HTTP requests and responses, a common requirement in web applications.
First, we define a middleware function wrap-logger
that logs the request method and URI, as well as the response status.
(defn wrap-logger [handler]
(fn [request]
;; Log the incoming request
(println "Request:" (:request-method request) (:uri request))
;; Call the original handler
(let [response (handler request)]
;; Log the response status
(println "Response status:" (:status response))
;; Return the response
response)))
To apply the middleware, we wrap it around our handler function. In a Clojure web application, handlers are typically defined using libraries like Ring and Compojure.
(require '[ring.adapter.jetty :refer [run-jetty]]
'[ring.middleware.defaults :refer [wrap-defaults site-defaults]])
(defn handler [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello, World!"})
(def app
(-> handler
wrap-logger
(wrap-defaults site-defaults)))
(run-jetty app {:port 3000})
In this example, wrap-logger
is applied to the handler
, and the resulting application is run on a Jetty server.
Authentication is another common use case for middleware. Let’s create a simple authentication middleware that checks for a specific header in the request.
(defn wrap-authentication [handler]
(fn [request]
(if (= "secret-token" (get-in request [:headers "authorization"]))
(handler request)
{:status 401
:headers {"Content-Type" "text/plain"}
:body "Unauthorized"})))
This middleware checks if the authorization
header contains the value "secret-token"
. If it does, the request is passed to the handler; otherwise, a 401 Unauthorized response is returned.
(def app
(-> handler
wrap-authentication
wrap-logger
(wrap-defaults site-defaults)))
(run-jetty app {:port 3000})
Here, wrap-authentication
is applied before wrap-logger
, ensuring that only authenticated requests are logged.
Request throttling is used to limit the number of requests a client can make in a given time period. Let’s implement a simple throttling middleware.
(def request-count (atom {}))
(defn wrap-throttle [handler]
(fn [request]
(let [client-ip (:remote-addr request)
current-count (get @request-count client-ip 0)]
(if (< current-count 10)
(do
(swap! request-count update client-ip (fnil inc 0))
(handler request))
{:status 429
:headers {"Content-Type" "text/plain"}
:body "Too Many Requests"}))))
This middleware uses an atom to keep track of the number of requests from each client IP. If the count exceeds 10, a 429 Too Many Requests response is returned.
(def app
(-> handler
wrap-throttle
wrap-authentication
wrap-logger
(wrap-defaults site-defaults)))
(run-jetty app {:port 3000})
Sometimes, you may need to transform requests or responses. Let’s create middleware that adds a custom header to every response.
(defn wrap-custom-header [handler]
(fn [request]
(let [response (handler request)]
(assoc-in response [:headers "X-Custom-Header"] "CustomValue"))))
This middleware adds an X-Custom-Header
to every response.
(def app
(-> handler
wrap-custom-header
wrap-throttle
wrap-authentication
wrap-logger
(wrap-defaults site-defaults)))
(run-jetty app {:port 3000})
Now that we’ve explored several examples of custom middleware, try modifying the code to:
To better understand how middleware functions are composed and executed, let’s visualize the flow of data through middleware layers.
graph TD; A[Incoming Request] --> B[wrap-authentication] B --> C{Authenticated?} C -->|Yes| D[wrap-throttle] C -->|No| E[Return 401 Unauthorized] D --> F{Request Limit?} F -->|Yes| G[wrap-logger] F -->|No| H[Return 429 Too Many Requests] G --> I[wrap-custom-header] I --> J[Handler] J --> K[Response]
Diagram Description: This flowchart illustrates the sequence of middleware execution. The request passes through authentication, throttling, logging, and custom header middleware before reaching the handler. Each middleware layer can modify the request or response.
wrap-logger
middleware to log request headers and response times.By mastering custom middleware in Clojure, you can build robust web applications that efficiently handle cross-cutting concerns. Now, let’s apply these concepts to create more sophisticated and responsive web applications.