Skip to Content
Course content

168: Extractors in Axum

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 a version: String field.
  • Path: A user_id of type u64.
  • Json: A UpdateUserRequest struct containing email: String and display_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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.