Tower in Miniature: HTTP Middleware from One Trait

When a request flows through a web app, it passes through a stack of middleware on the way in and unwinds back through them on the way out. Logging starts a timer, auth checks a token, the handler does the work, then auth and logging finish on the way back. It is an onion, and the request travels to the center and back.
Rust's tower and axum model this with a Service trait and Layers. Strip away the async and the generics and the essence fits on a postcard. Here it is, synchronous, by hand.
One trait: request in, response out
pub trait Service {
fn call(&self, req: Request) -> Response;
}
That is the entire abstraction. A handler is a Service. A whole middleware stack is also a Service. Anything that turns a Request into a Response qualifies, which means middleware and leaf handlers are the same kind of thing, and that is what makes them compose.
A plain closure should count as a handler, so we adapt one:
pub struct HandlerFn<F>(pub F);
impl<F: Fn(Request) -> Response> Service for HandlerFn<F> {
fn call(&self, req: Request) -> Response {
(self.0)(req)
}
}
A middleware wraps a service and returns a service
In Go this pattern is famously func(http.Handler) http.Handler. The Rust shape is the same idea with trait objects:
pub type BoxService = Box<dyn Service>;
pub type Middleware = Box<dyn Fn(BoxService) -> BoxService>;
A middleware takes the inner service and returns a new service that does its own work and then (usually) calls the inner one. Logging, for instance, wraps a service so that calling the wrapper logs, calls inward, and logs again. Crucially, a middleware can also decide not to call inward and return a response itself. That is how auth rejects a request: it sees a missing token and returns a 401 without ever touching the handler. Short-circuiting is just "do not call the inner service."
Composing the onion
pub fn chain(handler: BoxService, middlewares: Vec<Middleware>) -> BoxService {
let mut h = handler;
for mw in middlewares.into_iter().rev() { // wrap in reverse
h = mw(h);
}
h
}
The one detail that earns its comment is the .rev(). You want chain(h, [A, B, C]) to behave like A(B(C(h))): A is the outermost layer, first to see the request on the way in and last to touch the response on the way out. To build that, you wrap from the inside out: start with the handler, wrap it in C, then B, then A. Iterating the list in reverse makes A end up on the outside. Get the direction wrong and your auth middleware runs after your handler, which is exactly the kind of bug that ships.
Why this design is so durable
- Uniformity. Because a stack of middleware is itself a
Service, you can nest stacks, mount one router inside another, and pass either to anything expecting aService. The leaf and the tree have the same type. - Composition over configuration. Adding behavior is wrapping, not editing. New middleware is a new layer, and the order is explicit in the list.
- Short-circuiting falls out for free. Reject, cache-hit, redirect: all of them are "return a response without calling inward." No special mechanism needed.
This is tower in miniature. The real thing adds async (call returns a future), readiness (poll_ready), and richer error handling, but the bones are exactly this: a one-method trait, a function that wraps it, and the reverse-wrap that makes the first middleware outermost. Once you see it, every web framework's middleware stack stops being magic and starts being an onion you can build yourself.
Build it yourself. Solve the HTTP Middleware Chain challenge on barehands and get graded on correctness and speed. No crates, just the standard library.