The Rust Web Backend Model
A Rust web backend built on Axum is not one big request-handling function with middleware bolted on the side.
It is a tokio async runtime driving a tree of small, typed services, where the router, the handlers, and every piece of middleware all implement the same Service trait from Tower.
Understanding that single fact unlocks most of what looks unfamiliar in Axum code: why middleware is added with .layer() instead of .use(), why extractors look like magic function arguments, and why the whole stack composes so cleanly with gRPC servers built on tonic.
This page is the conceptual anchor for the rest of the web-backends section. The other pages here show you how to route requests, extract data, and add middleware; this page explains why those pieces are shaped the way they are, and how the runtime, router, and extractor layers interact underneath the syntax.
Summary
- A Rust web backend on Axum is an async runtime (tokio) executing a nested tree of Service implementations, where the router, every handler, and every piece of middleware are just services that transform a request into a response.
- Insight: This shared abstraction is what lets Axum,
tower-http, andtonic(gRPC) middleware interoperate, and it gives you typed, backpressure-aware composition instead of an untyped callback chain. - Key Concepts: Service trait, Layer trait, extractor, poll_ready and backpressure, ServiceBuilder, the tokio async runtime.
- When to Use: Building an HTTP API that needs shared behavior (logging, auth, timeouts, compression) across many routes, sharing middleware between HTTP and gRPC endpoints, or reasoning about why a request hung or a layer didn't run.
- Limitations/Trade-offs: The trait-based composition model has a steeper learning curve than a simple middleware array, and its generic-heavy types produce long compiler error messages when a service or layer doesn't line up.
- Related Topics: async runtimes, request/response lifecycles, dependency injection via typed state, gRPC service composition.
Foundations
At the bottom of every Axum application is an async runtime, almost always tokio.
The runtime is a scheduler for tasks, which are independently pollable units of async work, and a web server is fundamentally one task per accepted TCP connection (with further tasks spawned per request as needed).
#[tokio::main] sets this scheduler up for you, and axum::serve hands it a listener plus your application so it can drive the accept loop.
On top of the runtime sits the router.
An Axum Router maps a method and path pattern to a handler function, but the important detail is that the Router itself is not a special object with its own bespoke request-handling logic; it implements Tower's Service trait, the same trait a database connection pool wrapper or a rate limiter could implement.
That is the load-bearing idea in this whole page: everything that touches a request in an Axum app, the router, each handler, and each piece of middleware, is a Service, and services compose by one wrapping another.
The last piece is the extractor.
When a handler function takes Path<u64> or Json<CreateUser> as an argument, Axum is not doing reflection or magic; each of those types implements a trait (FromRequestParts or FromRequest) that knows how to pull its own piece of data out of the incoming request before your handler body ever runs.
Extractors are ordinary Rust types with an async parsing method, and the router calls them in argument order, short-circuiting into an error response the moment one fails.
A simple analogy: think of the router as a table of typed function signatures, and extractors as the argument-binding step that happens before the call.
Mechanics & Interactions
The Service trait has three key associated pieces: a call method that turns a request into a future of a response, a poll_ready method that reports whether the service is ready to accept work, and associated Response and Error types.
A Layer is a small factory that takes an inner Service and returns a new Service that wraps it, so applying middleware means constructing a new, larger service that contains the old one, not appending a function to a list.
This is why .layer() calls nest visually and semantically: the last layer you add becomes the outermost wrapper, so it sees the request first and the response last.
ServiceBuilder exists purely to make that nesting readable, letting you write layers top-to-bottom in the order they should apply to a request rather than reasoning about wrapping order in your head.
Because every layer must implement poll_ready, a slow or saturated inner service can signal back-pressure to everything wrapping it before a request is even accepted for processing, which is a stronger contract than most middleware-chain frameworks offer.
The snippet below shows the shape of a layer wrapping a service; the important part is that poll_ready and call both delegate to self.inner, so custom behavior is added around an existing service rather than inserted into a shared mutable pipeline.
// A Layer wraps an inner Service, returning a new Service - composition by
// nesting, not a callback chain like typical middleware stacks.
impl<S> Layer<S> for LogLayer {
type Service = LogService<S>;
fn layer(&self, inner: S) -> Self::Service { LogService { inner } }
}
impl<S: Service<Req>> Service<Req> for LogService<S> {
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
self.inner.poll_ready(cx) // backpressure propagates inward-out
}
fn call(&mut self, req: Req) -> Self::Future { self.inner.call(req) }
}Extractors interact with this model at a different point in the pipeline: they run inside the handler's own call, after all router-level and route-level middleware layers have already run.
That ordering explains a common point of confusion, that middleware added with .layer() cannot see a typed State extractor the way a handler can, because middleware operates on the raw Request before extraction has happened at all.
Route-scoped middleware added with .route_layer() sits between the router's dispatch and the handler's own extraction step, which is why it is the right tool when a piece of middleware genuinely needs to run only for specific routes.
Advanced Considerations & Applications
The Service/Layer model is not unique to HTTP: tonic, the standard gRPC library for Rust, uses the exact same Service and Layer traits, which means a tracing layer or a timeout layer written once against Tower can, in principle, wrap both an Axum HTTP router and a tonic gRPC server.
That interoperability is a direct, practical payoff of choosing a trait-based composition model over something bespoke to one web framework, and it is one of the reasons the wider async-Rust ecosystem converged on Tower rather than every framework inventing its own middleware format.
It also has implications for testing: because a Router is just a Service, you can drive it directly with tower::ServiceExt::oneshot in a unit test without binding a real TCP listener, which keeps test suites fast and avoids port conflicts in CI.
At scale, the nested-service model interacts with graceful shutdown and cancellation in a specific way: when a connection's task is dropped, the future returned by call is dropped too, and Rust's ownership model guarantees that resources held inside every layer along that call chain are cleaned up deterministically as the stack unwinds.
That is a meaningfully different guarantee than in runtimes where a request handler runs to completion regardless of client disconnect, and it is part of why axum::serve(...).with_graceful_shutdown(...) can drain in-flight requests cleanly rather than needing a separate cancellation-token convention layered on top.
Observability tooling, most notably tower_http::trace::TraceLayer, plugs into this same seam: because it is just another layer, it sees every request that reaches the router regardless of which handler eventually serves it, which makes it a natural place to emit span-per-request tracing.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Tower Service/Layer nesting (Axum, tonic) | Typed, backpressure-aware, shared across HTTP and gRPC | Generic-heavy types, steeper learning curve for custom layers | Apps that need reusable, testable middleware across multiple protocols |
Callback middleware chain (Express/Koa-style next()) | Familiar, easy to write inline, minimal ceremony | No compile-time backpressure signal, easy to forget calling next() and hang a request | Small scripts or teams already fluent in that style, ported quickly to Rust |
| Actix-web's own Service/Transform traits | Conceptually similar nesting model, tuned to actix's actor-based runtime | Its own trait shapes, less direct interop with the Tower/Axum/tonic ecosystem | Existing Actix-web codebases not sharing middleware with Tower-based services |
Common Misconceptions
- "Layer order is backwards" - it feels that way at first, but the last
.layer()call becomes the outermost wrapper, so it genuinely does run first on the way in and last on the way out; the mental model is nesting dolls, not a numbered list. - "Async handlers run in parallel automatically" - tokio schedules many tasks concurrently, but a single handler's
.awaitpoints are where it can be interrupted to let other work run, not a guarantee of parallel execution across CPU cores for that one request. - "Middleware can read typed State like a handler can" - middleware written with
.layer()operates on the raw request before extraction happens, so it cannot useState<T>the way a handler argument can; it has to reach into request extensions or capture config in a closure instead. - "Tower is an Axum-specific abstraction" - Tower predates Axum and is a general service abstraction also used by
tonicand other crates, which is exactly why middleware can be shared across an HTTP and a gRPC server in the same codebase. - "More layers always means more latency" - each layer adds a small amount of indirection, but because
poll_readyandcallare just trait dispatch with no allocation by default, the practical overhead of a handful of layers is usually negligible next to I/O costs like a database round trip.
FAQs
What exactly is "the Rust web backend model" this page is describing?
It is the combination of three layers working together: a tokio async runtime scheduling tasks, an Axum router that is itself a Tower Service, and typed extractors that parse a request into handler arguments before the handler body runs.
Why does Axum route requests through a Tower Service instead of calling handlers directly?
Making the router a Service means handlers, middleware, and the router share one trait, so anything that can wrap a Service (logging, timeouts, auth) can wrap any of them uniformly, including sub-routers and even a whole other application mounted as a fallback.
How does a request actually flow from the TCP listener to a handler?
- tokio accepts a connection and spawns a task for it.
- The outermost Tower layer's
callruns first, then delegates inward through each nested layer. - The
Routerdispatches on method and path to the matching handler's own service. - The handler's extractors run in argument order, parsing the request into typed values.
- The handler body runs and returns a value that implements
IntoResponse.
How does backpressure actually propagate through a layered stack?
Before call is ever invoked, the outer service's poll_ready calls the inner service's poll_ready, all the way down, so an inner service reporting "not ready" (for example, a connection pool at capacity) can stop a request from even starting to be processed by any layer above it.
What's the difference between a Tower `Layer` and Axum's `middleware::from_fn`?
middleware::from_fn is a convenience wrapper that lets you write an async function as middleware without manually implementing Service and Layer; under the hood it still produces a service that fits into the same nested composition model.
When should I skip the Service/Layer pattern and just write logic inline in a handler?
If a behavior is genuinely specific to one route and will never be reused or shared with another service, writing it directly in the handler is simpler and avoids the generic-heavy ceremony of a custom Layer; reach for a layer once two or more routes need the same cross-cutting behavior.
Is this model only useful for large applications?
No, but its main payoff (backpressure-aware, protocol-agnostic composition) is most visible once you have several routes and at least one piece of shared middleware; a single-route hello-world app won't feel the difference.
Does this model still apply if I use a raw `TcpListener` instead of `axum::serve`?
Yes conceptually, but axum::serve exists specifically to wire the tokio listener into this Service-based model correctly, including HTTP/1.1 and HTTP/2 handling, so bypassing it means reimplementing that wiring yourself.
Why can't a Tower layer use a `State` extractor the way a handler can?
State<T> is an extractor that runs during a handler's own request processing, after routing has already dispatched to that handler; a layer wraps the router or a route generically and runs earlier, before any handler-specific extraction, so it never receives the same typed context.
What happens if a middleware function never calls the inner service?
The request simply never reaches anything wrapped by that middleware, including the handler; this is the deliberate mechanism used for early-return behavior like rejecting an unauthenticated request with a 401 before it reaches route logic.
Is Tower's Service/Layer pattern the same thing gRPC servers use?
Yes, tonic (the standard Rust gRPC library) is built on the same Service and Layer traits from Tower, which is why middleware like tracing or timeouts can, in principle, be written once and applied to both an HTTP and a gRPC server in the same project.
What is the honest downside of this model compared to a simpler middleware array?
The generic types involved in custom Service and Layer implementations produce long, sometimes intimidating compiler errors when types don't line up, and newcomers often reach for middleware::from_fn specifically to avoid writing those trait implementations by hand.
Related
- Web Backends Basics - hands-on first server, extractor, and middleware examples
- Axum Routing & Handlers - how routes and methods map to handler services
- Extractors & State - the extractor traits this page's Foundations section summarizes
- Middleware & Tower - hands-on Layer and ServiceBuilder usage
- Tokio Basics - the async runtime this whole model runs on
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.