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
168: Extractors in Axum
I've always found Axum's extractors to be one of the most elegant parts of the framework, but they can be a bit mystical when you first encounter them. Essentially, an extractor is just a way of telling Axum: "I need this specific piece of data from the incoming request, and I want you to handle the parsing logic before my function even starts."
Instead of manually digging through a request object, we just add arguments to our handler function. If the extractor fails—say, a user sends a string where we expected an integer—Axum automatically returns a 400 Bad Request. It keeps our handler logic clean and focused on the business rules.
Capturing IDs and Filters
Let's build a small piece of a Task Management API. I want an endpoint that fetches tasks for a specific project, but I also want to allow the user to filter those tasks by status (like "completed" or "pending") via a query string.
use axum::{extract::{Path, Query}, routing::get, Router};
use serde::Deserialize;
#[derive(Deserialize)]
struct FilterParams {
status: Option<String>,
}
async fn get_tasks(
Path(project_id): Path<u32>,
Query(params): Query<FilterParams>
) -> String {
let status = params.status.unwrap_or_else(|| "all".to_string());
format!("Fetching tasks for project {} with status: {}", project_id, status)
}
let app = Router::new().route("/projects/:id/tasks", get(get_tasks));
I'm using Path to grab the :id from the URL and Query to map the URL parameters into a nice Rust struct. Notice how I'm using destructuring in the function signature—Path(project_id)—which is the idiomatic way to get the inner value immediately.
The "Body Consumed" Trap
Now, let's say we want to allow the user to send a JSON body containing a "search term" to further refine these results. I'll add the Json extractor to the mix. This is where I usually trip up when I'm rushing a project.
I'll try to write the handler like this:
#[derive(Deserialize)]
struct SearchQuery {
term: String,
}
async fn get_tasks(
Json(search): Json<SearchQuery>,
Path(project_id): Path<u32>,
Query(params): Query<FilterParams>,
) -> String {
// ... logic ...
}
If I try to compile this, I might be fine, but if I add another extractor that also needs to touch the request body—or if I try to access the raw Request object after Json—I'll hit a wall. Axum has a very strict rule: only one extractor can consume the request body.
Wait, I actually just made a classic mistake in my head. While Path and Query are fine anywhere, the Json extractor consumes the body stream. If I were to put Json before another extractor that also consumes the body (like Bytes or String), the code wouldn't even compile because the handler wouldn't implement Handler. The compiler error is famously cryptic here, usually saying something about the function not having the right signature.
The golden rule I follow now: Always put your body-consuming extractor (like Json, Form, or Bytes) as the very last argument in your handler.
Integrating Application State
Finally, we need a way to actually talk to a database. We don't want to create a new database connection inside every request. This is where the State extractor comes in. It allows us to inject shared data—like a connection pool—directly into the handler.
use axum::extract::State;
use std::sync::Arc;
struct AppState {
db_connection: String, // Simplified for this example
}
async fn get_tasks(
State(state): State<Arc<AppState>>,
Path(project_id): Path<u32>,
Query(params): Query<FilterParams>,
Json(search): Json<SearchQuery>,
) -> String {
format!(
"Using DB {} to find tasks for project {} with status {} and term {}",
state.db_connection, project_id, params.status.unwrap_or_default(), search.term
)
}
I've placed State first, followed by the metadata extractors (Path, Query), and finished with the body extractor (Json). This is the "safe" order that avoids almost all common Axum compile-time headaches. It reads logically: "Here is the environment I'm working in, here is where the request is coming from, and here is the data they sent me."
📋 Practical Task
Build a User Profile Update Handler
Your task is to create a handler function for updating a user profile. The endpoint should be PATCH /users/:user_id.
The handler must use the following extractors in the correct order to avoid "body consumed" errors:
- State: An
Arc<AppState>containing aversion: Stringfield. - Path: A
user_idof typeu64. - Json: A
UpdateUserRequeststruct containingemail: Stringanddisplay_name: String.
The handler should return a String that interpolates all these values (the app version, the user ID, and the new email) to confirm the update was processed. Ensure your imports are correct and the extractor order follows the "body last" rule.
There are no comments for now.