Overview
Axum is the framework the Tokio team built to prove the substrate works: routing and
handler ergonomics over hyper, with tower's Service as the middleware story
rather than a proprietary one. A handler is a plain async fn whose arguments are
extractors (types implementing FromRequest) and whose return implements
IntoResponse — no attribute macros, everything checked at compile time through
(sometimes hair-raising) trait bounds. Since its 2021 release it has become the
community default for new services.
Key points
- Extractors are the API:
Path<u32>,Query<Params>,Json<Body>,State<S>,HeaderMap, or your ownFromRequestPartsimpl (auth tokens, tenant IDs) — handler signatures document the request contract. IntoResponsecomposes the other direction: returnJson<T>,(StatusCode, Json<T>),Result<impl IntoResponse, AppError>— implementingIntoResponsefor your error type is the idiomatic error-handling move (map domain errors to problem+json in one place).- Router composition:
Router::new().route("/users/{id}", get(show).put(update)),.nest("/api", api_router),.merge(...)— plus.with_state(state)making the state type part of the router's type. - Middleware is tower:
.layer(TraceLayer::new_for_http()),TimeoutLayer,CorsLayer, compression, request-id — all from tower-http, none of it axum-specific; anything written for tower works, including your ownLayer. This is the compounding-interest argument from the correspondence table. - State is typed, not a bag:
State<AppState>(anArc-cloned struct) — misuse is a compile error, not aNoneat runtime; sub-routers can carry narrower state viaFromRef. - Testing without a socket: a
Routeris atower::Service, sorouter.oneshot(request).awaitdrives the full stack in-process — see Integration testing. - The error messages are the tax: a handler failing the trait bounds produces
infamous walls of text;
#[debug_handler]turns them into human diagnostics — know it exists. - WebSockets & SSE built in:
WebSocketUpgradeandSseextractor/response types — see WebSockets in Rust.
Examples
#[derive(Clone)]
struct AppState { pool: PgPool }
async fn show_user(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<User>, AppError> {
let user = users::find(&state.pool, id).await?;
Ok(Json(user))
}
let app = Router::new()
.route("/users/{id}", get(show_user))
.layer(TraceLayer::new_for_http())
.with_state(state);
axum::serve(TcpListener::bind("0.0.0.0:8080").await?, app).await?;
Related
- Rust web frameworks — the landscape — parent overview.
- Frameworks × tower/hyper — axum as the "honors everything" column.
- REST in Rust — axum in API-building practice: serde, utoipa, error shapes.
- Integration testing — oneshot-driving routers in tests.
- Async runtimes — the Tokio ground floor.