Middleware Patterns in Clojure Web Development

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.

What Middleware Is Good At

Middleware is strongest for cross-cutting concerns such as:

  • parameter parsing
  • keywordization
  • sessions and cookies
  • authentication boundaries
  • correlation IDs
  • CORS headers
  • compression or response shaping

These are concerns that many handlers need, but that should not be duplicated inside every endpoint.

Think in Terms of Request/Response Contracts

A good middleware layer makes the handler contract clearer:

  • it can guarantee parsed params exist
  • it can attach identity or correlation data
  • it can normalize response headers
  • it can enforce a shared error-handling boundary

That is useful because handlers can then depend on a smaller, more stable boundary contract.

Order Is Part of the Design

Because middleware wraps handlers, order is not cosmetic. It changes behavior. For example:

  • parsing must happen before code expects parsed params
  • authentication may need session or header processing first
  • error middleware should usually wrap a broad portion of the stack

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.

Request Enrichment vs. Business Decisions

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.

Response Shaping Can Also Belong Here

Middleware can standardize response behavior:

  • adding security headers
  • normalizing content types
  • attaching request IDs
  • compressing bodies
  • translating domain errors into HTTP-friendly responses

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.

Keep Wrapper Responsibilities Small

Small wrappers are easier to trust:

  • one wrapper for auth context
  • one wrapper for correlation IDs
  • one wrapper for body parsing
  • one wrapper for security headers

Large multi-purpose wrappers often create hidden dependencies because one piece quietly assumes another already ran.

Document Added Request Keys and Assumptions

Middleware is much easier to maintain when it is honest about the contract it creates. If a wrapper adds:

  • :identity
  • :request-id
  • parsed params
  • timing metadata

that 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.

Async and Middleware Still Need Clear Ownership

When parts of the stack become asynchronous, middleware design gets more delicate:

  • where are timeouts enforced?
  • which layer logs failures?
  • does the wrapper still behave correctly if the handler responds later?

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.

Common Failure Modes

Middleware goes bad when:

  • business logic is hidden in wrappers
  • middleware order is accidental
  • too many request keys are injected without documentation
  • one wrapper quietly changes the assumptions of another

Another common mistake is building middleware that does too much. Small focused wrappers are easier to test and compose.

Injecting Too Many Request Keys Without a Contract

Once handlers depend on a pile of undocumented request enrichments, the stack becomes hard to refactor safely.

Practical Heuristics

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.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026