Skip to main content
Axum is built on top of Tower, a library of modular and reusable components for building robust networking clients and servers. This deep integration gives you access to a rich ecosystem of middleware.

Understanding Tower

Tower provides three core abstractions:
  • Service: An asynchronous function from a request to a response
  • Layer: Middleware that wraps a service to modify its behavior
  • ServiceBuilder: A utility for composing layers
Axum’s Router implements tower::Service, so it works seamlessly with all Tower middleware.

Adding middleware with layers

Use the .layer() method to add Tower middleware:
use axum::Router;
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(TraceLayer::new_for_http());

Middleware execution order

Middleware executes in reverse order of how it’s added:
use tower_http::{
    compression::CompressionLayer,
    cors::CorsLayer,
    trace::TraceLayer,
};

let app = Router::new()
    .route("/api/data", get(get_data))
    .layer(CompressionLayer::new())  // Runs third (closest to handler)
    .layer(CorsLayer::permissive())  // Runs second
    .layer(TraceLayer::new_for_http()); // Runs first (outermost)
Execution flow:
  1. Request → TraceLayer → CorsLayer → CompressionLayer → Handler
  2. Handler → CompressionLayer → CorsLayer → TraceLayer → Response

tower-http middleware

The tower-http crate provides HTTP-specific middleware:

CORS

Handle Cross-Origin Resource Sharing:
use axum::{routing::get, Router, Json};
use tower_http::cors::{CorsLayer, Any};
use http::Method;

let cors = CorsLayer::new()
    .allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
    .allow_methods([Method::GET, Method::POST])
    .allow_headers(Any);

let app = Router::new()
    .route("/api/data", get(get_data))
    .layer(cors);
use tower_http::cors::{CorsLayer, AllowOrigin};
use http::{header, Method};

let cors = CorsLayer::new()
    .allow_origin(AllowOrigin::predicate(|origin, _| {
        // Custom origin validation logic
        origin.as_bytes().ends_with(b".example.com")
    }))
    .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
    .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
    .allow_credentials(true)
    .max_age(Duration::from_secs(3600));

Compression

Compress response bodies:
use tower_http::compression::CompressionLayer;
use tower_http::decompression::RequestDecompressionLayer;

let app = Router::new()
    .route("/api/data", get(large_data).post(receive_data))
    .layer(RequestDecompressionLayer::new())  // Decompress requests
    .layer(CompressionLayer::new());          // Compress responses
Supported algorithms: gzip, deflate, br (brotli), zstd

Tracing and logging

Log requests and responses:
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(TraceLayer::new_for_http());

Request timeouts

Set timeouts for requests:
use tower_http::timeout::TimeoutLayer;
use std::time::Duration;

let app = Router::new()
    .route("/slow", get(slow_handler))
    .layer(TimeoutLayer::new(Duration::from_secs(10)));
With custom error status:
use tower_http::timeout::TimeoutLayer;
use axum::http::StatusCode;

let app = Router::new()
    .route("/api/data", get(handler))
    .layer(
        TimeoutLayer::with_status_code(
            StatusCode::REQUEST_TIMEOUT,
            Duration::from_secs(30),
        )
    );

Request/response size limits

use tower_http::limit::RequestBodyLimitLayer;
use axum::extract::DefaultBodyLimit;

let app = Router::new()
    .route("/upload", post(upload_handler))
    // Disable default 2MB limit
    .layer(DefaultBodyLimit::disable())
    // Set custom limit (100MB)
    .layer(RequestBodyLimitLayer::new(100 * 1024 * 1024));

Static file serving

Serve static files:
use tower_http::services::{ServeDir, ServeFile};

let app = Router::new()
    .route("/api/hello", get(api_handler))
    // Serve files from ./assets directory
    .nest_service("/assets", ServeDir::new("assets"))
    // Serve a single file
    .route_service("/favicon.ico", ServeFile::new("assets/favicon.ico"));
With directory listings and index files:
use tower_http::services::ServeDir;

let serve_dir = ServeDir::new("assets")
    .append_index_html_on_directories(true)
    .precompressed_gzip()
    .precompressed_br();

let app = Router::new()
    .nest_service("/static", serve_dir);

ServiceBuilder for composing middleware

Use ServiceBuilder to organize multiple layers:
use tower::ServiceBuilder;
use tower_http::{
    compression::CompressionLayer,
    cors::CorsLayer,
    trace::TraceLayer,
    timeout::TimeoutLayer,
};
use std::time::Duration;

let middleware = ServiceBuilder::new()
    .layer(TraceLayer::new_for_http())
    .layer(TimeoutLayer::new(Duration::from_secs(10)))
    .layer(CompressionLayer::new())
    .layer(CorsLayer::permissive());

let app = Router::new()
    .route("/api/data", get(handler))
    .layer(middleware);
ServiceBuilder applies layers in the order they appear (unlike .layer() which applies them in reverse).

Per-route middleware

Apply middleware to specific routes:
use axum::{
    middleware::{self, Next},
    response::Response,
    extract::Request,
};

async fn auth_middleware(request: Request, next: Next) -> Response {
    // Check authentication
    let auth_header = request.headers()
        .get("Authorization")
        .and_then(|h| h.to_str().ok());
    
    if auth_header != Some("Bearer secret") {
        return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
    }
    
    next.run(request).await
}

let app = Router::new()
    .route("/public", get(public_handler))
    .route("/protected", get(protected_handler)
        .layer(middleware::from_fn(auth_middleware)));

Sharing state with middleware

Use extensions to share data:
use axum::{
    extract::{Extension, Request},
    middleware::Next,
    response::Response,
};
use uuid::Uuid;

#[derive(Clone)]
struct RequestId(String);

async fn set_request_id(mut request: Request, next: Next) -> Response {
    let request_id = RequestId(Uuid::new_v4().to_string());
    request.extensions_mut().insert(request_id.clone());
    
    let mut response = next.run(request).await;
    response.headers_mut().insert(
        "x-request-id",
        request_id.0.parse().unwrap(),
    );
    response
}

async fn handler(Extension(request_id): Extension<RequestId>) -> String {
    format!("Request ID: {}", request_id.0)
}

let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(set_request_id));

Custom Tower middleware

Create your own middleware:
use tower::{Layer, Service};
use std::task::{Context, Poll};
use std::future::Future;
use std::pin::Pin;

#[derive(Clone)]
struct MyMiddleware<S> {
    inner: S,
}

impl<S, B> Service<Request<B>> for MyMiddleware<S>
where
    S: Service<Request<B>>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request<B>) -> Self::Future {
        // Modify request here
        println!("Request: {} {}", request.method(), request.uri());
        self.inner.call(request)
    }
}

#[derive(Clone)]
struct MyLayer;

impl<S> Layer<S> for MyLayer {
    type Service = MyMiddleware<S>;

    fn layer(&self, inner: S) -> Self::Service {
        MyMiddleware { inner }
    }
}

let app = Router::new()
    .route("/", get(handler))
    .layer(MyLayer);

Common middleware patterns

use tower_http::request_id::{
    MakeRequestId, RequestId, PropagateRequestIdLayer, SetRequestIdLayer,
};
use uuid::Uuid;

#[derive(Clone)]
struct MakeRequestUuid;

impl MakeRequestId for MakeRequestUuid {
    fn make_request_id<B>(&mut self, _request: &Request<B>) -> Option<RequestId> {
        let id = Uuid::new_v4().to_string().parse().unwrap();
        Some(RequestId::new(id))
    }
}

let app = Router::new()
    .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
    .layer(PropagateRequestIdLayer::x_request_id());

Testing with Tower

#[cfg(test)]
mod tests {
    use tower::ServiceExt; // for `oneshot`
    use axum::{
        body::Body,
        http::{Request, StatusCode},
    };

    #[tokio::test]
    async fn test_with_middleware() {
        let app = Router::new()
            .route("/", get(|| async { "Hello" }))
            .layer(TraceLayer::new_for_http());

        let response = app
            .oneshot(Request::get("/").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }
}
Be careful with middleware ordering. Request processing flows from outer to inner layers, while response processing flows from inner to outer layers.
Use ServiceBuilder when composing many middleware layers - it’s more ergonomic and the layer order matches the execution order.