The tower-http Toolbox
Here's the mental model for the whole phase: tower-http is a box of ready-made Layers. In Phases 3 and 4 you learned the abstract machinery — a Service turns a request into a response, a Layer wraps a Service to make a new one. That was the theory. tower-http is where you cash it in. It's a crate full of pre-built Layers that do the things every real HTTP service needs — log requests, handle CORS, compress responses, enforce timeouts — and because they're plain tower Layers, they snap onto any HTTP tower service. Your axum app, a tonic gRPC server, a bare hyper service, even an HTTP client: same layers, same .layer(...) move.
That's the dividend the Service/Layer abstraction was paying toward the whole time. You don't write a tracing middleware. You don't write a CORS handler. You add a crate and hang a layer.
📝 This phase is the applied counterpart to Phase 4. Phase 4 showed you how
LayerandServiceBuildercompose middleware in the abstract; here you get the concrete, production-grade layers you'll actually reach for. The exact same crate, and the exact same layers, are what axum users add with.layer— see axum's middleware phase. We're looking at it from underneath.
Why tower-http works everywhere
tower-http is built on the http and http-body crates — the shared vocabulary types (Request, Response, body streams) that the whole Rust HTTP ecosystem agrees on. It is not built on axum, or hyper, or tonic specifically. It targets the lowest common denominator: "a tower Service whose request and response are http::Request / http::Response."
That's why one TimeoutLayer can wrap an axum router today and a hyper-based HTTP client tomorrow. The layer doesn't know or care what's inside — it only speaks http.
You add layers à la carte, enabling a Cargo feature per family of layers you want:
What just happened: each feature flag (trace, cors, compression, …) pulls in one group of layers and nothing else. tower-http is heavily feature-gated so you only compile the middleware you actually use. Forgetting the feature is the usual "why won't tower_http::trace resolve?" — the module is gated off until you enable its flag.
The key layers
Here's the toolbox, layer by layer. Each one is a Layer you construct and wrap around a service.
TraceLayer::new_for_http()— emits request and response events (method, path, status, latency) through thetracingcrate. The single most useful layer in the box.CorsLayer— adds CORS headers and handles preflightOPTIONSrequests.CorsLayer::permissive()allows everything (fine for local dev); the builder (CorsLayer::new().allow_origin(...)) locks it down for production.CompressionLayer— compresses response bodies (gzip, brotli, deflate, zstd) based on the client'sAccept-Encoding. Its mirror,DecompressionLayer, transparently decompresses request bodies.TimeoutLayer— aborts a request that runs longer than a setDuration, returning a408-style response instead of hanging forever. (toweritself also ships a generictimeout; thetower-httpone is HTTP-aware.)RequestBodyLimitLayer— rejects requests whose body exceeds a byte limit, so a client can't exhaust your memory by streaming a giant upload.SetResponseHeaderLayer— sets (or overrides) a response header on every response, e.g. acache-controlor a customx-powered-by.ServeDir— not a layer but a ready-madeServicethat serves static files from a directory. You mount it as a route's service rather than wrapping something.
use Duration;
use ;
let trace = new_for_http;
let cors = permissive;
let compress = new;
let timeout = new;
let body_limit = new; // 2 MiB
What just happened: every line constructs a Layer value — and nothing more. Constructing a layer does no work; it's just a recipe for how to wrap a service. None of these are attached to anything yet. That's the next step.
Composing the stack with ServiceBuilder
A real service wants several of these at once, in a deliberate order. As you saw in Phase 4, ServiceBuilder stacks layers so they read top-to-bottom — the first .layer(...) becomes the outermost wrapper, the one a request hits first on the way in.
use ServiceBuilder;
use ;
let service = new
.layer // outermost: logs everything inside it
.layer
.layer
.service; // your app / hyper service
What just happened: ServiceBuilder::new() starts an empty stack; each .layer(...) wraps another ring around whatever comes after it; .service(inner) plugs your actual service in at the center. The result is a new Service — inner wrapped in CORS, wrapped in compression, wrapped in tracing. A request flows in trace → compress → cors → inner, and the response unwinds back out in reverse. The ordering matters for real reasons: tracing is outermost so it times and logs everything, including the work compression does, and it sees responses before they're compressed into an opaque blob. (This is exactly the Phase 4 ordering rule — ServiceBuilder reads in execution order; bare chained .layer() calls read bottom-up.)
💡 Step back and feel the payoff. You wrote zero middleware. You got request tracing, response compression, and CORS — three things every production HTTP service needs — by adding a crate and stacking three values. That is what the
Service/Layerabstraction from Phases 3 and 4 buys you: an ecosystem of drop-in, composable middleware that works on anything shaped like an HTTP service.
⚠️ TraceLayer logs nothing on its own
The most common surprise with TraceLayer — and with any tracing-based layer — is that you add it, run your server, hit it with requests, and see no logs at all. Nothing is broken.
⚠️
TraceLayeremitstracingevents; it does not print them. Thetracingcrate splits those two jobs deliberately. Until you install a subscriber to receive and render events, they go nowhere. The fix is one line at startup:init;Put that at the top of
main, before you start serving, and yourTraceLayerevents appear on stderr. No subscriber, no output — every Rust web developer hits this once.
What just happened: tracing_subscriber::fmt::init() registers a global subscriber that formats events and writes them out. TraceLayer was doing its job all along — emitting structured events into the tracing system — but with no subscriber listening, there was nobody on the other end of the line. This separation is a feature: the same layer can feed a pretty dev console, structured JSON in production, or an OpenTelemetry pipeline, just by swapping the subscriber.
The same layers axum hands you
If you've used axum's middleware, none of this is new — and that's the point.
💡 When an axum guide tells you to
cargo add tower-http --features traceand write.layer(TraceLayer::new_for_http()), this is the crate it means. axum has no middleware of its own.TraceLayer,CorsLayer,CompressionLayer,TimeoutLayer— they'retower-httplayers, and they work on an axumRouterfor one reason only: aRouteris a towerService, so a towerLayerwraps it like any other. The skill transfers in both directions. Anything you learned reaching for layers in axum applies to a bare hyper service here; anything here applies straight back to axum. One abstraction, one toolbox, everywhere.
Recap
tower-httpis a box of ready-madeLayers — the concrete payoff of theService/Layerabstraction. You add a crate and hang a layer instead of writing middleware.- It's built on the shared
http/http-bodytypes, not on any one framework, so its layers work on any HTTP tower service: axum, tonic, a raw hyper service, even a client. - The staples:
TraceLayer(request/response tracing),CorsLayer(CORS + preflight),CompressionLayer/DecompressionLayer,TimeoutLayer,RequestBodyLimitLayer,SetResponseHeaderLayer, andServeDirfor static files. Enable each with acargo add tower-http --features ...flag. - Compose them with
ServiceBuilder(top-to-bottom = outermost-first, per Phase 4's ordering rule) or, on axum, with.layer(...). - ⚠️
TraceLayerprints nothing without atracing_subscriber— install one (tracing_subscriber::fmt::init()) at startup, or you'll see no logs. - 💡 These are the exact layers axum users add with
.layer— same crate, because aRouteris just another towerService(see axum).
Quick check
[
{
"q": "Why can a tower-http layer like TimeoutLayer wrap an axum app, a tonic gRPC server, AND an HTTP client?",
"choices": ["It has special-cased code for each framework", "It's built on the shared http/http-body types, so it works on any HTTP tower Service", "axum, tonic, and the client all re-export it", "It only works on axum; the others reimplement it"],
"answer": 1,
"explain": "tower-http targets the lowest common denominator: a tower Service over http::Request/http::Response. It doesn't know what's inside, so it wraps anything shaped that way."
},
{
"q": "You add TraceLayer::new_for_http(), send requests, and see no logs. What's wrong?",
"choices": ["TraceLayer is broken; use a different layer", "You forgot to call .layer()", "No tracing_subscriber is installed — TraceLayer emits events but a subscriber must render them", "Tracing only works in release builds"],
"answer": 2,
"explain": "TraceLayer emits tracing events; it doesn't print them. Without a subscriber (e.g. tracing_subscriber::fmt::init()) listening, the events go nowhere."
},
{
"q": "In a ServiceBuilder stack, which layer is the outermost — the one a request hits first on the way in?",
"choices": ["The last .layer() added", "The first .layer() added", "Whichever calls .service()", "Ordering is undefined in ServiceBuilder"],
"answer": 1,
"explain": "ServiceBuilder reads top-to-bottom: the first .layer() is the outermost wrapper and runs first inbound. (Bare chained .layer() calls are the reverse — bottom-up.)"
}
]
← Phase 4: Layers & Middleware · Guide overview · Phase 6: How axum Uses Them →
Check your understanding
1. Why can a tower-http layer like TimeoutLayer wrap an axum app, a tonic gRPC server, AND an HTTP client?
2. You add TraceLayer::new_for_http(), send requests, and see no logs. What's wrong?
3. In a ServiceBuilder stack, which layer is the outermost — the one a request hits first on the way in?