Axum Routing & Handlers
Define HTTP routes, bind handlers to methods, and return typed responses with Axum 0.8.
Search across all documentation pages
Define HTTP routes, bind handlers to methods, and return typed responses with Axum 0.8.
Quick-reference recipe card - copy-paste ready.
use axum::{routing::{get, post, delete}, Router, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)] struct CreateUser { email: String }
#[derive(Serialize)] struct User { id: u64, email: String }
async fn create(Json(body): Json<CreateUser>) -> Json<User> {
Json(User { id: 1, email: body.email })
}
let app = Router::new()
.route("/users", get(list_users).post(create))
.route("/users/:id", get(get_user).delete(delete_user));When to reach for this: You need explicit HTTP method routing with typed request and response bodies.
use axum::{
Router, extract::{Path, State},
http::StatusCode,
routing::{delete, get, post},
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]
struct Item { id: u64, name: String }
#[derive(Deserialize)]
struct NewItem { name: String }
type Store = std::sync::Arc<tokio::sync::RwLock<Vec<Item>>>;
async fn list(State(store): State<Store>) -> Json<Vec<Item>> {
Json(store.read().await.clone())
}
async fn create(
State(store): State<Store>,
Json(body): Json<NewItem>,
) -> (StatusCode, Json<Item>) {
let mut guard = store.write().await;
let item = Item { id: guard.len() as u64 + 1, name: body.name };
guard.push(item.clone());
(StatusCode::CREATED, Json(item))
}
async fn remove(Path(id): Path<u64>, State(store): State<Store>) -> StatusCode {
let mut guard = store.write().await;
if let Some(pos) = guard.iter().position(|i| i.id == id) {
guard.remove(pos);
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let store = Store::default();
let app = Router::new()
.route("/items", get(list).post(create))
.route("/items/:id", delete(remove))
.with_state(store);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}What this demonstrates:
(StatusCode::CREATED, Json(item)) for precise responses.Path and State extractors combined in one handler signature.RwLock store for a runnable demo without a database.Router is a tower::Service that matches requests by path and method.IntoResponse - strings, tuples, Json, StatusCode, or custom types.| Pattern | Example | Notes |
|---|---|---|
| Static | /health | Exact match |
| Param | /users/:id | Captured as Path<T> |
| Wildcard | /files/*path | Remaining segments as one param |
| Fallback | .fallback(handler) | 404 or SPA shell |
use axum::response::Redirect;
async fn old() -> Redirect { Redirect::permanent("/v2/old") }/users/me before /users/:id.State without .with_state() panic at runtime. Fix: Always call .with_state() on the final router before serving.tokio::task::spawn_blocking or async database drivers.Allow header in custom 405 handlers if needed.#[serde(rename = "camelCase")] or return explicit 422 mapping.| Alternative | Use When | Don't Use When |
|---|---|---|
| Actix-web | Team prefers actor model or mature ecosystem | You want Tower ecosystem composability |
| Rocket | Procedural macros and compile-time routing appeal | You need minimal magic and stable async traits |
| hyper directly | Building a custom HTTP stack | Standard REST APIs with routing and extractors |
Use .fallback(handler) on the router.
Yes with async fn wrapping sync code via spawn_blocking, but prefer async I/O.
Nest under /v1 with Router::nest.
Merge with Router::merge or nest under prefixes.
Add utoipa or aide - not built into Axum.
Usually at the load balancer; Axum serves HTTP behind a TLS proxy.
Path for resource identity; query for optional filters and pagination.
Return (StatusCode, [(HeaderName, &str)], Json(body)).
Use axum::body::Body or tower_http::services::ServeDir.
Use tower::ServiceExt::oneshot without binding a port.
Add tower_http::cors::CorsLayer.
Apply tower_governor or a custom Tower layer.
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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026