rgoussu@goussu: ~/library/rust/web-frameworks
~/library/rust/web-frameworks cat axum.md

Axum deep dive

# The Tokio team's framework — extractors and IntoResponse, Router composition, State, tower/tower-http middleware for free, and the FromRequest machinery.

Conceptsaved 2026-08-09 #rust#web#axum#tokio#tower

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 own FromRequestParts impl (auth tokens, tenant IDs) — handler signatures document the request contract.
  • IntoResponse composes the other direction: return Json<T>, (StatusCode, Json<T>), Result<impl IntoResponse, AppError> — implementing IntoResponse for 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 own Layer. This is the compounding-interest argument from the correspondence table.
  • State is typed, not a bag: State<AppState> (an Arc-cloned struct) — misuse is a compile error, not a None at runtime; sub-routers can carry narrower state via FromRef.
  • Testing without a socket: a Router is a tower::Service, so router.oneshot(request).await drives 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: WebSocketUpgrade and Sse extractor/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