Extractors allow you to pull data from HTTP requests in a type-safe way. They implement either the FromRequestParts or FromRequest trait.
Extractors are types that implement FromRequest or FromRequestParts. When used as handler arguments, Axum automatically extracts the data:
use axum::{
extract::{Path, Query},
http::HeaderMap,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Params {
page: u32,
}
async fn handler(
Path(id): Path<u32>,
Query(params): Query<Params>,
headers: HeaderMap,
) -> String {
format!("ID: {}, Page: {}", id, params.page)
}
Axum provides many built-in extractors:
Path
Query
Json
Form
State
Extract data from the URL path:use axum::{Router, routing::get, extract::Path};
use serde::Deserialize;
// Single parameter
async fn get_user(Path(id): Path<u32>) -> String {
format!("User ID: {}", id)
}
// Multiple parameters as tuple
async fn get_team(
Path((user_id, team_id)): Path<(u32, u32)>
) -> String {
format!("User {} in team {}", user_id, team_id)
}
// Multiple parameters as struct
#[derive(Deserialize)]
struct TeamParams {
user_id: u32,
team_id: u32,
}
async fn get_team_struct(
Path(TeamParams { user_id, team_id }): Path<TeamParams>
) -> String {
format!("User {} in team {}", user_id, team_id)
}
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/team/:team_id", get(get_team));
Extract query parameters:use axum::{Router, routing::get, extract::Query};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
async fn list_items(
Query(pagination): Query<Pagination>
) -> String {
let page = pagination.page.unwrap_or(1);
let per_page = pagination.per_page.unwrap_or(10);
format!("Page {}, {} items per page", page, per_page)
}
let app = Router::new()
.route("/items", get(list_items));
// Handles: GET /items?page=2&per_page=20
Parse JSON request bodies:use axum::{Router, routing::post, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
async fn create_user(
Json(payload): Json<CreateUser>
) -> Json<User> {
Json(User {
id: 1,
name: payload.name,
email: payload.email,
})
}
let app = Router::new()
.route("/users", post(create_user));
Parse URL-encoded form data:use axum::{Router, routing::post, Form};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Input {
name: String,
email: String,
}
async fn accept_form(
Form(input): Form<Input>
) -> String {
format!("Name: {}, Email: {}", input.name, input.email)
}
let app = Router::new()
.route("/subscribe", post(accept_form));
Access shared application state:use axum::{
Router,
routing::get,
extract::State,
};
#[derive(Clone)]
struct AppState {
counter: u32,
}
async fn handler(
State(state): State<AppState>
) -> String {
format!("Counter: {}", state.counter)
}
let state = AppState { counter: 0 };
let app = Router::new()
.route("/", get(handler))
.with_state(state);
See State for more details.
The order of extractors in your handler signature matters!
Extractors are processed in order:
FromRequestParts extractors come first (Path, Query, State, headers, etc.)
FromRequest extractors come last (body extractors like Json, Form, Bytes)
use axum::{
extract::{Path, Query, State},
Json,
http::HeaderMap,
};
#[derive(Clone)]
struct AppState {}
// ✅ Correct order
async fn correct(
State(state): State<AppState>, // FromRequestParts
Path(id): Path<u32>, // FromRequestParts
Query(params): Query<Params>, // FromRequestParts
headers: HeaderMap, // FromRequestParts
Json(payload): Json<CreateUser>, // FromRequest (body)
) {}
// ❌ Wrong - body extractor before Path
async fn wrong(
Json(payload): Json<CreateUser>, // FromRequest - too early!
Path(id): Path<u32>, // Won't compile
) {}
Use Option<T> to make an extractor optional:
use axum::{
extract::{Path, Query},
Router,
routing::get,
};
async fn handler(
path: Option<Path<String>>,
query: Option<Query<Params>>,
) -> String {
match (path, query) {
(Some(Path(p)), Some(Query(q))) =>
format!("Path: {}, Query: {:?}", p, q),
_ => "Missing data".to_string(),
}
}
Extract the entire request:
use axum::{
extract::Request,
http::Method,
};
async fn handler(request: Request) -> String {
format!("Method: {}, URI: {}", request.method(), request.uri())
}
Request is a FromRequest extractor, so it must come last (or second-to-last if using Next in middleware).
FromRequest vs FromRequestParts
Understanding the difference:
Extractors that only need access to the request head (method, URI, headers, extensions):
Path
Query
State
HeaderMap
Method
Uri
Multiple FromRequestParts extractors can be used in any order.
Extractors that consume the request body:
Json
Form
Bytes
String
Request
Only one FromRequest extractor can be used per handler, and it must come last (except for Next in middleware).
Create your own extractors by implementing FromRequestParts or FromRequest:
use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, StatusCode},
};
struct ApiToken(String);
#[async_trait]
impl<S> FromRequestParts<S> for ApiToken
where
S: Send + Sync,
{
type Rejection = StatusCode;
async fn from_request_parts(
parts: &mut Parts,
_state: &S
) -> Result<Self, Self::Rejection> {
if let Some(auth) = parts.headers.get("authorization") {
if let Ok(value) = auth.to_str() {
return Ok(ApiToken(value.to_string()));
}
}
Err(StatusCode::UNAUTHORIZED)
}
}
// Use in handlers
async fn protected(token: ApiToken) -> String {
format!("Token: {}", token.0)
}
Derive macros
Use the FromRequest derive macro to create custom extractors more easily:use axum::{
extract::{FromRequest, rejection::JsonRejection},
http::StatusCode,
response::{IntoResponse, Response},
};
// Custom JSON extractor with better error messages
#[derive(FromRequest)]
#[from_request(via(axum::Json), rejection(AppError))]
struct AppJson<T>(T);
struct AppError(JsonRejection);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(StatusCode::BAD_REQUEST, "Invalid JSON").into_response()
}
}
Next steps
State
Learn how to share state across handlers
Responses
Understand how to return different response types