10. Extractors
📋 Jump to Takeaways🎁 Every parameter in an Axum handler that isn't a plain type is an extractor. Path, Query, Json, State — they all implement the same trait. You can write your own, and that's where the real power is.
How Extractors Work
An extractor implements FromRequestParts (for things that only need headers/path/query) or FromRequest (for things that need the body). Axum calls them in order before your handler runs.
If any extractor fails, the handler never runs — Axum returns the rejection response immediately.
Built-in Extractors Recap
async fn handler(
State(state): State<AppState>, // app state
Path(id): Path<i64>, // /items/:id
Query(params): Query<Pagination>, // ?page=1&limit=10
Json(body): Json<CreateItem>, // request body
) -> impl IntoResponse {
// all four extracted before this runs
}Order matters for FromRequest extractors (like Json) — they consume the body, so they must come last.
Custom Extractor: Validated Input
The Json extractor deserializes but doesn't validate. Wrap it to add validation:
use axum::{extract::{FromRequest, Request}, response::{IntoResponse, Response}};
use serde::de::DeserializeOwned;
use validator::Validate;
pub struct ValidatedJson<T>(pub T);
impl<T, S> FromRequest<S> for ValidatedJson<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<T>::from_request(req, state)
.await
.map_err(|e| e.into_response())?;
value.validate()
.map_err(|e| AppError::InvalidInput(e.to_string()).into_response())?;
Ok(ValidatedJson(value))
}
}Add validator = "0.18" to Cargo.toml, then use it on your request structs:
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct CreateLink {
#[validate(url)]
pub url: String,
#[validate(length(max = 50))]
pub label: Option<String>,
}
async fn create_link(
State(state): State<AppState>,
ValidatedJson(body): ValidatedJson<CreateLink>,
) -> Result<(StatusCode, Json<Link>), AppError> {
// body.url is guaranteed valid here
let link = create(&state.pool, &body.url).await?;
Ok((StatusCode::CREATED, Json(link)))
}Custom Extractor: Auth Token
Extract and verify a bearer token without repeating the logic in every handler:
use axum::{extract::FromRequestParts, http::{request::Parts, StatusCode}};
pub struct AuthUser {
pub user_id: String,
}
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let token = parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
// verify_token(token, secret) — full implementation with state in lesson 12
let user_id = verify_token(token, "secret")
.map(|claims| claims.sub)
.ok_or(StatusCode::UNAUTHORIZED)?;
Ok(AuthUser { user_id })
}
}Now any handler that declares AuthUser as a parameter is automatically protected:
async fn delete_link(
State(state): State<AppState>,
AuthUser { user_id }: AuthUser, // 401 if no valid token
Path(code): Path<String>,
) -> Result<StatusCode, AppError> {
// only reaches here with a valid token
delete_by_code(&state.pool, &code, user_id).await?;
Ok(StatusCode::NO_CONTENT)
}Key Takeaways
- All handler parameters except plain types are extractors implementing
FromRequestorFromRequestParts FromRequestPartsreads headers/path/query without consuming the body;FromRequestcan read the bodyJsonextractors must come last — they consume the body- Custom extractors centralize logic like validation and authentication so handlers stay clean
- If any extractor's
from_requestreturnsErr, the handler never runs
🎁 Your service works. Now: how do you know it keeps working after you change something? Next up: testing Axum handlers directly, without spinning up a server.