Axum Middleware With Tower Layers and From Fn Compared
Add behavior around every request with Tower layers, or write your own with axum middleware from_fn. See the order layers run in and when to pick each form.

Cross-cutting behavior in a web service, such as request logging, authentication, timeouts and compression, does not belong inside handlers. Axum gives it a home by building on Tower, the middleware framework from the Rust async ecosystem, and exposes two ways in: a quick function-based form for your own logic and the full Layer machinery for sharing it. The surprise for newcomers is ordering. Layers wrap each other like onion skins, and two different ways of attaching them run in opposite orders. This post builds a timing layer, an auth layer that hands the user to handlers, shows the order with real output, and compares from_fn with writing a Tower layer. Compiled with axum 0.8.9, tower 0.5 and tower-http 0.6 on Rust 1.99.
Middleware as an async function
A middleware function takes the request and a Next, calls next.run(req).await to run everything inside it (other layers, then the handler), and returns a response. Code before the call sees the request; code after it sees the response.
use std::time::Instant;
use axum::{
Extension, Router,
extract::Request,
http::{StatusCode, header},
middleware::{self, Next},
response::Response,
routing::get,
};
#[derive(Clone, Debug)]
struct CurrentUser(String);
async fn timing(req: Request, next: Next) -> Response {
let path = req.uri().path().to_owned();
let start = Instant::now();
let res = next.run(req).await;
println!("{path} -> {} in {}us", res.status(), start.elapsed().as_micros());
res
}
async fn require_token(mut req: Request, next: Next) -> Result<Response, StatusCode> {
let token = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
match token {
Some("let-me-in") => {
req.extensions_mut().insert(CurrentUser("ada".into()));
Ok(next.run(req).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
async fn me(Extension(user): Extension<CurrentUser>) -> String {
format!("you are {}", user.0)
}
async fn health() -> &'static str {
"ok"
}
#[tokio::main]
async fn main() {
let private = Router::new()
.route("/me", get(me))
.route_layer(middleware::from_fn(require_token));
let app = Router::new()
.route("/health", get(health))
.merge(private)
.layer(middleware::from_fn(timing));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}curl -i localhost:3000/me
curl localhost:3000/me -H 'authorization: Bearer let-me-in'
curl -i localhost:3000/nopeHTTP/1.1 401 Unauthorized
you are ada
HTTP/1.1 404 Not FoundThe middleware's own line, printed by the server for each request, looks like /me -> 401 Unauthorized in 145us, and the timing layer saw all three requests, including the 404. Two mechanics are worth noting. A middleware may return a Result whose error implements IntoResponse, which is how require_token rejects without calling next. And it passes information inward by inserting a value into the request's extensions; the me handler pulls it back out with the Extension extractor. If the middleware were missing, the extractor would return a 500 saying the extension is absent, a sign of a wiring mistake and not a client error.
Which layer runs first
Ordering is the part to get right. Each .layer() wraps everything added before it, so the last one added is the outermost and sees the request first. Two logging middleware functions make it visible. Here log_a prints "A before" and "A after" around next.run, and log_b the same with B:
let app = Router::new()
.route("/health", get(health))
.layer(middleware::from_fn(log_a))
.layer(middleware::from_fn(log_b));B before
A before
A after
B afterB was added last, so B is outermost: the request meets B, then A, then the handler, and the response travels back A then B. This reads backwards to most people, because source order and execution order differ. tower::ServiceBuilder fixes that by reading top to bottom in execution order, and it is the form Axum's documentation recommends once there is more than one layer:
use tower::ServiceBuilder;
let app = Router::new()
.route("/health", get(health))
.layer(
ServiceBuilder::new()
.layer(middleware::from_fn(log_a))
.layer(middleware::from_fn(log_b)),
);A before
B before
B after
A afterWith the builder, A is outermost and the order you read is the order requests travel. Use it whenever order matters, and note it in a comment, because the failure mode is subtle: put a timeout inside a retry layer and each attempt gets the full timeout; reverse them and the whole retry sequence shares one. Authentication should sit inside tracing (so rejected requests are still logged) and outside anything expensive, such as body parsing or compression, so a bad token is refused cheaply. A sensible default stack from outside in: request id, tracing, timeout, compression, CORS, authentication.
layer versus route_layer
On a Router, .layer(...) wraps the whole router including its fallback, so the middleware runs for requests that match no route. .route_layer(...) wraps only the routes already defined, and an unmatched path skips it. The difference decides what a client sees for a typo:
| Attached with | Request to /nope | Request to /me without a token |
|---|---|---|
.layer(require_token) | 401, the auth layer rejected before routing found nothing | 401 |
.route_layer(require_token) | 404, the router never reached the layer | 401 |
With layer, every misspelled URL says "unauthorized", which leaks nothing but misleads clients and tools that probe for routes. For auth, route_layer on the protected sub-router is usually what you mean, merged into the main router as in the example. Layers attached to a nested router apply only to that subtree, which is the natural way to split public and private APIs; routing explained covers nesting.
Ready-made layers beat hand-written ones
Most of what you need already exists in tower-http, tested against edge cases you would rediscover: TraceLayer for structured request logs, TimeoutLayer, CompressionLayer, CorsLayer, RequestBodyLimitLayer, SetResponseHeaderLayer and PropagateRequestIdLayer. Adding a response header, for example, is one line and no function:
use axum::http::{HeaderName, HeaderValue};
use tower_http::set_header::SetResponseHeaderLayer;
let stack = ServiceBuilder::new().layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-served-by"),
HeaderValue::from_static("demo"),
));The dedicated guides cover the ones with sharp edges: CORS, timeouts and body limits and rate limiting. Writing a custom Tower Layer and Service is only needed when you ship middleware as a library or need state across the call that from_fn cannot hold cleanly; Tower services and layers explained walks through the types. from_fn can also take extractors before the Next argument (State, HeaderMap), and from_fn_with_state passes the shared state in, which is how an auth layer reaches a database pool.
Passing state into middleware
A middleware that needs the application state, such as an auth check that consults a session store, uses from_fn_with_state. The state is extracted in the function's first arguments the same way a handler does, with the request and Next last:
use std::sync::Arc;
use axum::{extract::State, middleware};
#[derive(Clone)]
struct AppState {
api_keys: Arc<Vec<String>>,
}
async fn check_key(
State(state): State<AppState>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let key = req.headers().get("x-api-key").and_then(|v| v.to_str().ok());
match key {
Some(k) if state.api_keys.iter().any(|a| a == k) => Ok(next.run(req).await),
_ => Err(StatusCode::UNAUTHORIZED),
}
}
let state = AppState { api_keys: Arc::new(vec!["k-123".into()]) };
let app = Router::new()
.route("/data", get(data))
.route_layer(middleware::from_fn_with_state(state.clone(), check_key))
.with_state(state);The state is passed twice, once to the middleware constructor and once to with_state for the handlers; that repetition is a clone of an Arc-based struct, so it costs nothing. Note the shortcut in the example: the key comparison uses ==, fine for a demonstration and wrong for secrets, where a constant-time comparison from constant-time comparison is the right tool. Place the middleware with route_layer as discussed above so unknown paths still return 404.
What middleware cannot do well
Middleware runs for every matched request on its router, so work that only some handlers need is cheaper as an extractor, which runs only where it appears. Do not read the request body in middleware unless you must: the body is a one-shot stream, and a middleware that buffers it to inspect it costs memory on every request and has to put it back. And do not use extensions as a hidden parameter list; if a handler cannot work without a CurrentUser, say so in its signature with a custom extractor, as in extractors and the order that matters, so the compiler stops a route being registered without its middleware. The token handling itself, including why constant-time comparison matters, is in JWT validation mistakes, and the full structure is in an Axum production service.


