Building Web APIs with Axum

Axum routing and extractors, handlers returning IntoResponse, State sharing, middleware via tower, error handling, and graceful shutdown.

IntermediateUpdated 2026-10-2835K views

Routing and Extractors

use axum::{
    routing::{get, post},
    extract::{Path, Query, State},
    Json, Router,
};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct PageQuery { page: Option<u32> }

#[derive(Deserialize)]
struct CreateUser { name: String }

#[derive(Serialize)]
struct User { id: u64, name: String }

// 提取器按声明顺序执行,最后一个是 body/请求本身
async fn list(Query(q): Query<PageQuery>) -> Json<Vec<User>> {
    Json(/* ... */)
}

async fn create(State(db): State<Db>, Json(body): Json<CreateUser>) -> Json<User> {
    Json(User { id: 1, name: body.name })
}

async fn detail(Path(id): Path<u64>) -> Json<User> { /* ... */ }

let app: Router = Router::new()
    .route("/users", get(list).post(create))
    .route("/users/:id", get(detail));
// Rust 2018+ 可用 /users/{id} 写法

Extractors are the core idea: anything implementing FromRequest becomes a typed function argument — Query for query strings, Path for segments, Json for a deserialized body (rejecting malformed input with a 400 automatically).

Responses and Errors

use axum::response::{IntoResponse, Response};
use http::StatusCode;

// 元组 -> (状态码, 头, body)
async fn json_ok() -> Response {
    (StatusCode::OK, [("x-cache", "hit")], "ok").into_response()
}

// 自定义错误类型:实现 IntoResponse
enum ApiError {
    NotFound,
    Forbidden,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (st, msg) = match self {
            ApiError::NotFound  => (StatusCode::NOT_FOUND, "not found"),
            ApiError::Forbidden => (StatusCode::FORBIDDEN, "forbidden"),
        };
        (st, msg).into_response()
    }
}

// 处理器直接返回 Result<T, ApiError>
async fn get_user() -> Result<Json<User>, ApiError> {
    Err(ApiError::NotFound)
}

State and Middleware

use std::sync::Arc;
use tower::ServiceBuilder;
use tower_http::{trace::TraceLayer, cors::CorsLayer};

#[derive(Clone)]
struct AppState {
    db: Arc<PgPool>,          // Arc 共享,Clone 成本低
}

let state = AppState { db: Arc::new(pool) };

let app = Router::new()
    .route("/users", get(list))
    .layer(
        ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())   // 日志
            .layer(CorsLayer::permissive())       // CORS
    )
    .with_state(state);

// 中间件本质是包裹 Service 的 async fn
async fn auth(req, next: Next) -> Result<Response, StatusCode> {
    if req.headers().get("token").is_none() {
        return Err(StatusCode::UNAUTHORIZED);
    }
    Ok(next.run(req).await)
}

Graceful Shutdown

async fn shutdown_signal() {
    use tokio::signal;
    let ctrl_c = async { signal::ctrl_c().await.unwrap(); };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .unwrap().recv().await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }
    println!("shutdown received");
}

let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app)
    .with_graceful_shutdown(shutdown_signal())
    .await?;
In production behind a reverse proxy, combine graceful shutdown with connection draining: stop accepting new connections, let in-flight requests finish, then exit — this is what zero-downtime rolling deploys require.

Friends & Partners