Skip to content

Extractors

In Express you access route parameters through the untyped req.params object. Axum replaces this with the Path<T> extractor — it deserializes the URL segment directly into a Rust type, failing at the type level if the value does not match.

TypeScript
// Express
app.get('/users/:id', async (req, res) => {
const id = req.params.id; // string, always
const numId = parseInt(id, 10); // manual parse, can fail at runtime
const user = await db.findUser(numId);
res.json(user);
});
Rust
use axum::{extract::Path, Json};
use uuid::Uuid;
async fn get_user(Path(id): Path<Uuid>) -> Json<User> {
// id is already a Uuid — Axum returns 400 automatically
// if the path segment is not a valid UUID
let user = db_find_user(id).await;
Json(user)
}

Express exposes query string parameters via req.query — again untyped. Axum’s Query<T> deserializes the query string into a struct using serde.

TypeScript
// Express
app.get('/users', async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 30;
const users = await db.listUsers({ page, limit });
res.json(users);
});
Rust
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
#[serde(default = "default_page")]
page: u32,
#[serde(default = "default_limit")]
limit: u32,
}
fn default_page() -> u32 { 1 }
fn default_limit() -> u32 { 30 }
async fn list_users(Query(pagination): Query<Pagination>) -> Json<Vec<User>> {
let users = db_list_users(pagination.page, pagination.limit).await;
Json(users)
}

Express parses JSON bodies with express.json() middleware and attaches the result to req.body — still untyped unless you cast it. Axum’s Json<T> extractor deserializes the body directly into your struct. Missing required fields → automatic 422 Unprocessable Entity.

TypeScript
// Express + zod for validation
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
app.post('/users', async (req, res) => {
const body = CreateUserSchema.parse(req.body); // throws if invalid
const user = await db.createUser(body);
res.status(201).json(user);
});
Rust
use axum::{http::StatusCode, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u32,
name: String,
email: String,
}
async fn create_user(
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
// payload is guaranteed to have name and email
let user = User { id: 42, name: payload.name, email: payload.email };
(StatusCode::CREATED, Json(user))
}

A single handler can accept multiple extractors as separate parameters. Axum calls them all before the handler body runs.

use axum::{
extract::{Path, Query, State},
Json,
};
use uuid::Uuid;
async fn get_user_posts(
State(db): State<DbPool>,
Path(user_id): Path<Uuid>,
Query(pagination): Query<Pagination>,
) -> Json<Vec<Post>> {
let posts = db.user_posts(user_id, pagination.page, pagination.limit).await.unwrap();
Json(posts)
}

Run locally — extractors only work in the context of a running Axum router. Use cargo run and test with curl.

What does `Path(id): Path<Uuid>` do in an Axum handler?
Which extractor should you use for JSON request bodies?
What HTTP status does Axum return when a `Json<T>` body fails to deserialize?
In Axum, how do you read query string parameters with defaults?