Learn how Ring middleware should be used in Clojure web applications, including ordering, request enrichment, response shaping, and the line between cross-cutting concerns and business logic.
Middleware: A higher-order function that wraps a Ring handler and can transform the request before the handler runs or the response after the handler returns.
Middleware is one of the most powerful ideas in the Ring ecosystem, but it is easy to overuse. The right middleware makes request handling consistent and composable. The wrong middleware hides business logic in layers that become hard to reason about and harder to test.
Middleware is strongest for cross-cutting concerns such as:
These are concerns that many handlers need, but that should not be duplicated inside every endpoint.
A good middleware layer makes the handler contract clearer:
That is useful because handlers can then depend on a smaller, more stable boundary contract.
Because middleware wraps handlers, order is not cosmetic. It changes behavior. For example:
If the order is unclear, the request lifecycle is unclear.
One practical review question is: “What request shape does the handler actually receive after the middleware stack finishes?” If no one can answer that easily, the middleware design is already too implicit.
Another useful question is: “Which wrapper owns this concern?” If logging, auth context, parsing, and error translation are all partially handled by several wrappers, the stack is already drifting toward ambiguity.
A good middleware layer often enriches the request:
1(defn wrap-request-id [handler]
2 (fn [request]
3 (handler (assoc request :request-id (str (java.util.UUID/randomUUID))))))
That is different from making core business decisions such as whether an order should be approved or a refund should be issued. Those usually belong in the handler or domain layer, not the middleware stack.
Middleware should enrich or normalize the boundary. It should not become a secret policy engine.
That distinction matters even more as the application grows. Hidden policy logic in wrappers often survives longer than hidden policy logic in handlers because it is harder to see during ordinary route review.
Middleware can standardize response behavior:
This is often more maintainable than repeating the same response boilerplate in every route.
The key is to keep the response concern generic. A wrapper that adds a request ID header is easy to understand. A wrapper that decides invoice approval semantics while also shaping JSON errors is not.
Small wrappers are easier to trust:
Large multi-purpose wrappers often create hidden dependencies because one piece quietly assumes another already ran.
Middleware is much easier to maintain when it is honest about the contract it creates. If a wrapper adds:
:identity:request-idthat should be visible in code or local documentation near the wrapper stack. Otherwise handlers start depending on invisible state that only exists because of wrapper order.
When parts of the stack become asynchronous, middleware design gets more delicate:
The answer is usually not “put everything in middleware.” It is to keep wrapper responsibilities narrow enough that asynchronous control flow does not hide who owns the boundary.
Middleware goes bad when:
Another common mistake is building middleware that does too much. Small focused wrappers are easier to test and compose.
Once handlers depend on a pile of undocumented request enrichments, the stack becomes hard to refactor safely.
Use middleware for cross-cutting web concerns, not domain rules. Keep each wrapper narrow, document any request keys it adds, and treat ordering as part of the system design rather than as incidental plumbing.