Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
51: Middleware in Axum
At some point in every project, you'll find yourself writing the same three lines of code at the start of every single handler. Maybe you're checking an API key, logging the request time, or extracting a trace ID from a header. When I first started with Axum, my instinct was to just build a helper function and call it at the top of every handler. It feels straightforward, and it's easy to trace with "Go to Definition" in your IDE.
The manual wrapper trap
Imagine we want to assign a unique Request ID to every incoming call so we can track logs across different services. The naive way is to write a helper and call it manually. It looks something like this:
async fn handle_user_profile(req: Request) -> Response {
let req_id = generate_request_id(&req);
info!("Processing request {}", req_id);
// Actual business logic here...
Response::new("User Profile")
}
This works for two or three routes. But as your API grows to fifty routes, this becomes a nightmare. You'll inevitably forget to add the call to a new endpoint, and suddenly your logs have gaps. Worse, if you decide to change how Request IDs are generated or stored, you're performing a find-and-replace across your entire codebase. You're essentially treating your cross-cutting concerns as business logic, which they aren't.
Leveraging the middleware stack
The better way is to move this logic into the middleware layer. Axum is built on top of Tower, which treats every request as a series of layers—like an onion. Instead of calling a helper inside the handler, we wrap the handler in a function that executes before the handler even knows a request has arrived.
The most ergonomic way to do this in Axum is using axum::middleware::from_fn. This allows us to write a standard async function that takes a Request and a Next object. The Next object is the magic part; it represents the rest of the chain. When you call next.run(req).await, you're handing the request off to the next piece of middleware or the final handler.
async fn track_request_id(
mut request: Request,
next: Next,
) -> Response {
let req_id = Uuid::new_v4().to_string();
// We can inject the ID into request extensions so handlers can access it
request.extensions_mut().insert(RequestId(req_id.clone()));
info!("Started request {}", req_id);
let response = next.run(request).await;
info!("Finished request {}", req_id);
response
}
Where the trade-offs live
You might wonder why we don't just do this everywhere. There is a cost to this abstraction. When you use middleware, you're moving away from explicit code paths. A developer looking at a handler function can no longer see every single thing that happens to a request just by reading that function. The "magic" happens in the router configuration.
However, the trade-off is almost always worth it for infrastructure concerns. By applying the middleware in the router, you ensure consistency:
let app = Router::new()
.route("/profile", get(handle_user_profile))
.route("/settings", get(handle_settings))
.layer(middleware::from_fn(track_request_id));
Now, every route defined above that .layer call automatically gets a Request ID. I prefer this approach because it separates how the server behaves (logging, auth, rate limiting) from what the server does (fetching a user profile). It keeps your handlers lean and focused on business logic, which makes them significantly easier to test in isolation.
📋 Practical Task
Build an API Key Validation Guard
Your task is to implement a middleware function that protects a set of routes. Instead of checking for an API key inside every handler, you will create a middleware layer that validates a request header.
Requirements:
- Create a middleware function using
axum::middleware::from_fn. - The middleware must check for a header named
X-API-Key. - If the header is missing or the value is not
"secret-123", the middleware should return aStatusCode::UNAUTHORIZEDimmediately, without callingnext.run(req).await. - Apply this middleware to a group of "admin" routes (e.g.,
/admin/dashboardand/admin/settings), but ensure that a public route (e.g.,/) remains accessible without the key.
There are no comments for now.