The Ring Request-Response Pipeline

Learn how Ring models HTTP as request and response maps, how middleware wraps handlers, and how a request moves inward while the response moves back out through the pipeline.

Ring: The foundational Clojure web interface that treats an HTTP request as a map and a handler as a function from request to response.

Ring matters because it keeps web programming close to ordinary data transformation. A request comes in as a map. A handler returns a response map. Middleware wraps handlers and can inspect or transform both directions of the flow. Once you understand that pipeline, most of the Clojure web ecosystem becomes easier to reason about.

The Core Contract

A basic Ring handler looks like this:

1(defn hello-handler [request]
2  {:status 200
3   :headers {"content-type" "text/plain; charset=utf-8"}
4   :body "hello"})

The request is just data. The response is just data. That simplicity is the main reason Ring has remained such a strong foundation for Clojure web work.

Why This Contract Scales Well

Because the boundary is just data plus functions, it is easy to:

  • test handlers directly
  • compose middleware incrementally
  • inspect request and response shapes in development
  • swap frameworks above Ring without changing the core contract

That small surface area is one of Ring’s biggest strengths for long-lived applications.

It is also why Ring remains such a strong teaching model. Even when a team later adopts richer routers, async handlers, or higher-level abstractions, the core request/response contract still explains most of the system’s behavior.

What Lives in the Request and Response Maps

The request map typically includes keys such as:

  • :request-method
  • :uri
  • :query-string
  • :headers
  • :params after parameter middleware runs
  • :body for streaming or request payload handling

The response map usually includes:

  • :status
  • :headers
  • :body

That contract is deliberately small. Frameworks add conveniences, but they still build on this shape.

How the Pipeline Actually Moves

The visual below shows the useful mental model: the request travels inward through middleware layers to the handler, and the response travels back outward through those same layers.

Ring request-response pipeline with middleware layers and request/response maps

This is why middleware order matters. The outermost wrapper sees the request first and the response last.

Request Inward, Response Outward

One of the easiest mistakes for new Ring users is imagining middleware as a flat list of features. It is really nested function composition. That means:

  • the request is progressively enriched on the way in
  • the handler runs at the center
  • the response is progressively shaped on the way out

Once that mental model is stable, debugging middleware order becomes much easier.

It also makes it easier to answer design questions that often get blurred together:

  • what is guaranteed to exist on the request before the handler runs?
  • which layer is allowed to attach identity or correlation data?
  • where are domain errors translated into HTTP responses?
  • where is logging or timing attached?

Middleware Is Just Handler Wrapping

1(defn wrap-request-id [handler]
2  (fn [request]
3    (let [request-id (str (java.util.UUID/randomUUID))
4          response   (handler (assoc request :request-id request-id))]
5      (assoc-in response [:headers "x-request-id"] request-id))))

Middleware is not a special magical layer. It is ordinary higher-order function composition around the handler. That makes it powerful, but it also means you need discipline about what belongs there.

That same model applies when the handler is asynchronous. The plumbing may differ, but the design question stays familiar: which layer owns request preparation, which layer owns the handler decision, and which layer shapes the response or error on the way back out?

Draw the Boundary Between Middleware and Handlers

The handler should usually own:

  • domain invocation
  • core decision making
  • final business result selection

Middleware should usually own:

  • request parsing
  • auth context attachment
  • correlation IDs
  • generic response shaping
  • shared error mapping

The cleaner that boundary stays, the easier it is to inspect the pipeline during debugging or code review.

This boundary is what keeps the Ring stack composable instead of mysterious.

A Small Realistic Pipeline

1(def app
2  (-> hello-handler
3      wrap-request-id
4      wrap-params
5      wrap-keyword-params))

Read that from bottom to top for request setup and from top to bottom for response unwinding. The order becomes easier to understand once you picture the nesting rather than imagining a flat list of features.

Ring Makes Boundary Testing Honest

Because both the request and response are just data, you can inspect the boundary at several levels:

  • a bare handler
  • a handler plus selected middleware
  • the assembled application stack

That is one reason Ring-based systems are often easier to test incrementally than heavier frameworks that bury the web boundary in container internals.

Common Failure Modes

Ring pipelines become confusing when:

  • middleware order is accidental
  • request maps are mutated conceptually in too many places
  • business logic leaks into middleware
  • error handling is duplicated at multiple layers

Another common mistake is forgetting that some request keys only exist after specific middleware has run.

Treating the Pipeline Like a Flat Feature List

If the team cannot explain which wrappers run first and what contract they produce, the application is already relying on accidental composition.

Practical Heuristics

Keep the handler focused on application behavior. Use middleware for cross-cutting concerns such as parsing, auth boundaries, request IDs, sessions, or response shaping. When debugging web behavior in Clojure, start by asking two questions: what did the request map look like at this layer, and which middleware wrapped the handler before it got here?

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026