Introduction to Middleware & tower
Learning Objectives: By the end of this lesson, you will be able to:
- Explain the concept and role of middleware in Axum and the tower ecosystem
- Implement and compose middleware in a Rust web application
- Identify and avoid common mistakes when working with middleware
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
What’s one thing you wish every web API would do automatically for you? (e.g., log every request, add security headers, or handle errors gracefully?)
Lesson Overview
- Duration: 90 minutes
- Prerequisites: Basic Rust knowledge, familiarity with async/await, and simple web server concepts
- Materials Needed: Rust toolchain, code editor, internet access, and a sample Axum project
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- What is middleware? - Understand the concept and its purpose
- Middleware in Axum and tower - How it works and how to use it
- Live coding: Adding and composing middleware
- Hands-on practice and exercises
- Common pitfalls and how to avoid them
Success Criteria:
By the end of this session, you should be able to explain, implement, and troubleshoot middleware in Axum applications.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is Middleware?
Middleware are components that wrap around your handlers to add extra functionality to your web application. In Axum, middleware is built on top of the tower library, which provides the Service and Layer traits. Middleware can intercept, modify, or act on requests and responses as they pass through the stack.
Real-World Analogy: Think of middleware like airport security checkpoints. Each checkpoint (middleware) can inspect, modify, or reject your request (luggage) before you reach your destination (handler). You can have multiple checkpoints, each with a specific job.
Why This Matters: Middleware lets you keep your core logic clean and focused, while handling cross-cutting concerns (like logging, authentication, or CORS) in reusable, composable layers.
The tower Ecosystem
- tower is a library for building robust, composable networking clients and servers. It provides the
ServiceandLayertraits, which Axum uses for its middleware system. - Middleware in Axum is just a special kind of
Layerthat wraps aService(your handler).
Key Terminology
- Middleware: A component that can intercept, modify, or act on requests/responses as they pass through the stack.
- Layer: A trait from tower that wraps a service with additional behavior (i.e., middleware).
- Service: A trait representing an asynchronous operation on a request, producing a response.
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Middleware in Axum
Middleware in Axum is added using the .layer() method on a Router. You can use built-in middleware from crates like tower-http, or write your own using middleware::from_fn. Middleware receives the request, can perform actions before and after calling the next handler, and can modify the request or response.
Practical Applications
- Add headers to every response (e.g., security or CORS headers)
- Log requests and responses
- Check authentication or permissions
- Handle errors or transform responses
Common Misconceptions
- Misconception: Middleware is only for logging. Reality: Middleware can handle a wide range of cross-cutting concerns, not just logging.
- Misconception: Middleware order doesn’t matter. Reality: The order you add middleware affects how requests and responses are processed.
5. Live Coding Demonstration (20-25 minutes)
Example 1: Basic Implementation
use axum::{Router, routing::get, middleware};
async fn hello() -> &'static str {
"Hello, Axum!"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.layer(middleware::from_fn(|req: axum::extract::Request, next: middleware::Next| async move {
println!("Request: {} {}", req.method(), req.uri());
let response = next.run(req).await;
println!("Response sent");
response
}));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Explanation:
- The
hellohandler returns a greeting. middleware::from_fncreates a custom middleware that logs the request method and URI before passing the request to the next handler, and logs after the response is sent.- The middleware is added to the router using
.layer(). - The server listens on
127.0.0.1:3000.
Example 2: Practical Application (Adding a Custom Header)
use axum::{Router, routing::get, middleware, response::IntoResponse};
async fn hello() -> &'static str {
"Hello, Axum!"
}
async fn add_custom_header(req: axum::extract::Request, next: middleware::Next) -> impl IntoResponse {
let mut response = next.run(req).await;
response.headers_mut().insert("X-Custom", "Hello".parse().unwrap());
response
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.layer(middleware::from_fn(add_custom_header));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Key Points:
- Demonstrates how to modify the response in middleware.
- Shows how to add custom headers to every response.
- Uses Axum 0.8’s concrete
Requestand non-genericNexttypes.
Example 3: Advanced Technique (Stacking Middleware)
use axum::{Router, routing::get, middleware, response::IntoResponse};
async fn hello() -> &'static str {
"Hello, Axum!"
}
async fn log_request(req: axum::extract::Request, next: middleware::Next) -> impl IntoResponse {
println!("Request: {} {}", req.method(), req.uri());
next.run(req).await
}
async fn add_custom_header(req: axum::extract::Request, next: middleware::Next) -> impl IntoResponse {
let mut response = next.run(req).await;
response.headers_mut().insert("X-Custom", "Hello".parse().unwrap());
response
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.layer(middleware::from_fn(log_request))
.layer(middleware::from_fn(add_custom_header));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Best Practices:
- Keep middleware focused on a single responsibility (e.g., logging, authentication, etc.).
- Use existing middleware crates (like
tower-http) when possible to avoid reinventing the wheel. - Place middleware in the correct order: the order you add them matters!
Debugging Demonstration
Show common errors and how to fix them:
// Common mistake: Forgetting to call next.run(req).await
.layer(middleware::from_fn(|req: axum::extract::Request, _next: middleware::Next| async move {
println!("Request received");
// Missing: next.run(req).await
// This will block the request from reaching your handler!
(axum::http::StatusCode::UNAUTHORIZED, "Blocked").into_response()
}))
// Corrected version
.layer(middleware::from_fn(|req: axum::extract::Request, next: middleware::Next| async move {
println!("Request received");
let response = next.run(req).await;
response
}))
6. Hands-On Practice (30-40 minutes)
Exercise 1: Basic Practice (10 minutes)
Objective: Write a middleware that adds a custom header to every response.
Instructions:
- Create a new Axum project or use an existing one.
- Write a middleware function that adds the header
X-Custom: Helloto every response. - Add the middleware to your router using
.layer(). - Test your endpoint with
curlor your browser and check the response headers.
Starter Code:
// ...existing code...
async fn add_custom_header(req: axum::extract::Request, next: middleware::Next) -> impl IntoResponse {
// Your code here
}
// ...existing code...
Solution:
async fn add_custom_header(req: axum::extract::Request, next: middleware::Next) -> impl IntoResponse {
let mut response = next.run(req).await;
response.headers_mut().insert("X-Custom", "Hello".parse().unwrap());
response
}
Exercise 2: Intermediate Challenge (15 minutes)
Objective: Log the response status code after the response is returned.
Requirements:
- Middleware should print the status code of every response.
- Should work for all routes.
- Should not modify the response body.
Hints:
- Use
response.status()afternext.run(req).await. - Remember to return the response.
Exercise 3: Creative Application (15 minutes)
Objective: Write a middleware that checks for a specific header in the request (e.g., X-Auth-Token) and returns a 401 Unauthorized response if it’s missing.
Challenge:
- Implement the middleware and add it to your router.
- Test with and without the header to see the difference.
Extension Ideas:
- Make the token value configurable.
- Log failed authentication attempts.
- Combine with other middleware for more complex flows.
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Forgetting to call
next.run(req).awaitin custom middleware Cause: The request never reaches your handler Solution: Always callnext.run(req).awaitunless you intend to short-circuit the request -
Problem: Adding middleware in the wrong order Cause: Middleware order affects processing (e.g., logging after authentication may miss unauthorized requests) Solution: Plan and document your middleware stack order
Debugging Tips
- Use
println!or logging crates to trace middleware execution - Check response headers and status codes to verify middleware effects
- Read error messages carefully—often they point to missing trait bounds or type mismatches
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Use middleware for cross-cutting concerns only
- Prefer existing, well-tested middleware crates when possible
- Keep middleware functions small and focused
- Document the order and purpose of each middleware in your stack
Next Steps in Your Learning Journey
- Explore the
tower-httpcrate for more middleware options - Learn about extracting data from requests using Axum extractors
- Try building more complex middleware (e.g., rate limiting, request tracing)
9. Summary and Review (5 minutes)
Key Takeaways
- Middleware enables clean, reusable, and composable handling of cross-cutting concerns
- Axum and tower provide a powerful, flexible middleware system
- Middleware order and correct usage are critical for robust applications
Quick Review Questions
- What is the purpose of middleware in a web application?
- How do you add middleware to an Axum router?
- Why does the order of middleware matter?
For Instructors
Teaching Tips
- Use real-world analogies to explain middleware
- Encourage students to experiment with middleware order and composition
- Address common confusion around
next.run(req).await - Adjust timing based on student experience
Assessment Ideas
- Quiz: Identify what a given middleware does
- Project: Build a small Axum app with at least two custom middleware
- Peer review: Have students review each other’s middleware for clarity and correctness
Adaptation Notes
- For beginners: Focus on simple middleware and lots of hands-on practice
- For advanced students: Dive into custom Layer/Service implementations
- Online: Use live coding and screen sharing; In-person: Pair programming and group discussion
- Adjust time for exercises based on class size and experience