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:
Request → TraceLayer → CorsLayer → CompressionLayer → Handler
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 );
Advanced CORS configuration
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:
Basic tracing
Custom tracing
Fully custom tracing
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
Request ID tracking
Rate limiting (with tower-governor)
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.