Extractors & State
Axum handlers receive request data through extractors - State, Json, Path, Query, headers, and more.
Recipe
Quick-reference recipe card - copy-paste ready.
use axum::extract::{Path, Query, State};
use axum::Json;
use serde::Deserialize;
#[derive(Clone)]
struct AppState { pool: sqlx::PgPool }
#[derive(Deserialize)]
struct Search { q: Option<String> }
async fn handler(
State(state): State<AppState>,
Path(id): Path<u64>,
Query(search): Query<Search>,
Json(body): Json<CreateDto>,
) -> Result<Json<Item>, AppError> {
// use state.pool, id, search.q, body
}When to reach for this: Every Axum handler that reads path, query, body, or shared application data.
Working Example
use axum::{
extract::{Path, Query, State},
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
items: Arc<tokio::sync::RwLock<Vec<String>>>,
}
#[derive(Deserialize)]
struct Filter { prefix: Option<String> }
#[derive(Serialize)]
struct ItemView { index: usize, name: String }
async fn list(
State(state): State<AppState>,
Query(filter): Query<Filter>,
) -> Json<Vec<ItemView>> {
let items = state.items.read().await;
let views: Vec<_> = items
.iter()
.enumerate()
.filter(|(_, name)| {
filter.prefix.as_ref().map(|p| name.starts_with(p)).unwrap_or(true)
})
.map(|(i, name)| ItemView { index: i, name: name.clone() })
.collect();
Json(views)
}
async fn get_one(Path(index): Path<usize>, State(state): State<AppState>) -> Json<ItemView> {
let items = state.items.read().await;
let name = items.get(index).cloned().unwrap_or_default();
Json(ItemView { index, name })
}
#[tokio::main]
async fn main() {
let state = AppState {
items: Arc::new(tokio::sync::RwLock::new(vec!["alpha".into(), "beta".into()])),
};
let app = Router::new()
.route("/items", get(list))
.route("/items/:index", get(get_one))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}What this demonstrates:
StatesharesArc-wrapped data across handlers.Querydrives optional filtering without new routes.Pathcaptures resource identifiers from the URL.- Multiple extractors compose in one handler signature.
Deep Dive
How It Works
- Extractors implement
FromRequestPartsorFromRequestand run before the handler body. State<T>clonesTper request - useArcfor pools, clients, and config.- Failed extraction maps to HTTP errors (400 for bad path/query, 422 for JSON).
Common Extractors
| Extractor | Source | Typical use |
|---|---|---|
Path<T> | URL segments | Resource IDs |
Query<T> | Query string | Filters, pagination |
Json<T> | Body | POST/PATCH payloads |
State<T> | Router state | DB pool, config |
HeaderMap | Headers | Auth tokens, tracing |
Rust Notes
// Optional extractor - entire param can be absent
use axum::extract::OptionalQuery;
async fn maybe(Query(q): OptionalQuery<Filter>) -> String { /* ... */ }Gotchas
- State not Clone -
AppStatewith non-Clone fields fails to compile. Fix: Wrap inArcorArc<Mutex<_>>. - Extractor order -
Bodyextractors consume the body; only one body extractor per handler. Fix: Use a singleJsonor customFromRequest. - State on nested routers - Sub-routers inherit state from
.with_state()on the parent. Fix: Call.with_state()once on the top-level router. - Query deserialization strictness - Unknown fields fail by default. Fix: Add
#[serde(deny_unknown_fields)]intentionally or allow extras. - Path type mismatch -
/users/:idwithPath<String>when clients send numbers. Fix: Pick the correct type or validate manually.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Request extensions | Per-request metadata from middleware | Shared app-wide config |
Extension<T> (deprecated pattern) | Legacy Tower apps | New Axum 0.8 projects - prefer State |
Manual Request parsing | Full control over body streaming | Standard JSON APIs |
FAQs
State vs Extension?
Axum 0.8 uses State for application data; avoid deprecated Extension for new code.
Multiple state types?
Combine into one AppState struct - Axum supports one state type per router.
Per-request DB transaction?
Open in handler from pool in State, or use middleware that inserts a transaction extension.
Auth extractor?
Implement FromRequestParts for AuthUser reading the Authorization header.
Multipart uploads?
Use axum::extract::Multipart for file fields.
Raw body bytes?
Use Bytes extractor from axum::body.
Validate query params?
Use validator crate on the Query struct or manual checks in the handler.
Nested State in tests?
Build router with .with_state(test_state) in each test module.
Extract cookies?
Use axum_extra::extract::CookieJar from the axum-extra crate.
Rejection customization?
Implement IntoResponse on custom rejection types for extractors.
Related
- Web Backends Basics - Intro examples
- Axum Routing & Handlers - Route definitions
- Request Validation - Field constraints
- Authentication & Sessions - Auth extractors
- Error Handling in Handlers - Extractor errors
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.