Extractors
From req.params to Path<T>
Section titled “From req.params to Path<T>”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.
// Expressapp.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);});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)}From req.query to Query<T>
Section titled “From req.query to Query<T>”Express exposes query string parameters via req.query — again untyped. Axum’s Query<T> deserializes the query string into a struct using serde.
// Expressapp.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);});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)}From req.body to Json<T>
Section titled “From req.body to Json<T>”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.
// Express + zod for validationimport { 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);});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))}Combining multiple extractors
Section titled “Combining multiple extractors”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 runand test withcurl.