Rust 中使用 Axum 的终极指南:从 Hello World 到生产环境

更新于 2026-01-16

Joshua Mo 2025-07-04

在 Rust Web 生态系统中,有众多后端 Web 框架可供选择,让人难以抉择。在过去,你可能会看到 Rocket 在流行度排行榜上遥遥领先;而如今,通常是 AxumActix Web 之间的较量,且 Axum 正逐渐占据上风。

在本文中,我们将深入探讨 Axum —— 一个由 Tokio 团队打造、专为构建 Rust REST API 而设计的 Web 应用框架。它简单易用,并与 Tower(一个用于构建网络应用的可复用、模块化组件库)高度兼容。

Axum 在 Rust 编程生态中的突出之处在于:

  • 无宏(macro-free)的 API 设计
  • 可预测的错误处理模型
  • 基于 Tower 构建的自有中间件系统

无论你是编写单一路由处理器,还是构建复杂的 API,Axum 的设计都能最大限度地减少样板代码,同时赋予你完全的控制权。


快速概览(TL;DR)

  • 路由与处理器(Routing & Handlers):使用 axum::Router 定义路由,并编写异步处理器函数。
  • 状态管理(State Management):通过 axum::extract::Statestd::sync::Arc 安全地共享状态(如数据库连接池)。
  • 中间件(Middleware):充分利用整个 towertower-http 生态系统,实现强大且可复用的中间件。
  • 测试(Testing):使用 tower::ServiceExt 直接高效地测试处理器,无需启动服务器。

本文将全面介绍如何使用 Axum 编写 Web 服务。内容已更新至 Axum 0.8Tokio 1.0,反映了最新的最佳实践。


使用 Axum 构建 Rust REST API 入门

Axum 专为在 Rust 生态中构建 REST API 而设计。我们从路由和处理器的基础开始。

Axum 中的路由与处理器函数

Axum 遵循类似 Express 的 REST 风格 API 设计:你可以创建异步函数处理器,并将其附加到 axum::Router 类型上。路径参数语法直观,且 Rust 编译器能在编译时帮助捕获错误。

例如,一个路由可能如下所示:

async fn hello_world() -> &'static str {
    "Hello world!"
}

然后我们可以将其添加到 Router 中:

use axum::{Router, routing::get};

fn init_router() -> Router {
    Router::new()
        .route("/", get(hello_world))
}

要使一个处理器函数有效,其返回值必须是 axum::response::Response 类型,或实现 axum::response::IntoResponse trait。该 trait 已为大多数基本类型及 Axum 自有类型实现。例如,若想向用户返回 JSON 响应,只需使用 Axum 的 Json 类型作为返回类型,将要发送的数据包裹其中即可。如上所示,我们也可以直接返回一个字符串(切片),几乎无需样板代码。

你也可以直接使用 impl IntoResponse,乍看之下似乎能立即解决“需要返回什么类型”的问题。然而,直接使用它意味着所有返回类型必须是同一类型!这可能导致不必要的错误。更好的做法是为枚举或结构体实现 IntoResponse,然后将其作为返回类型。如下所示:

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

// 这里展示一个实现了 Serialize + Send 的类型
#[derive(Serialize)]
struct Message {
    message: String
}

enum ApiResponse {
    OK,
    Created,
    JsonData(Vec<Message>),
}

impl IntoResponse for ApiResponse {
    fn into_response(self) -> Response {
        match self {
            Self::OK => (StatusCode::OK).into_response(),
            Self::Created => (StatusCode::CREATED).into_response(),
            Self::JsonData(data) => (StatusCode::OK, Json(data)).into_response()
        }
    }
}

这种模式允许你根据应用逻辑声明式地解析请求,并返回相应的 HTTP 状态码响应。

然后在处理器函数中使用该枚举:

async fn my_function() -> ApiResponse {
    // ... 其余代码
}

当然,我们也可以使用 Result 类型作为返回值!虽然错误类型理论上可以接受任何能转换为 HTTP 响应的类型,但我们也可以像处理成功响应一样,实现一个错误响应类型,以清晰表达应用中 HTTP 请求可能失败的多种方式。这能为你在整个应用中提供可预测的错误处理模型。如下所示:

enum ApiError {
    BadRequest,
    Forbidden,
    Unauthorised,
    InternalServerError
}

// ... 在此处实现 IntoResponse

async fn my_function() -> Result<ApiResponse, ApiError> {
    // ... 你的代码
}

这使得我们在编写 Axum 路由时能够清晰区分错误与成功请求,从而在整个 Web 应用框架中实现健壮的错误处理。


Axum 处理器中的错误处理

对于构建可靠的 Rust Web 应用而言,恰当的错误处理至关重要。如前所述,Axum 处理器可以返回带有自定义错误响应的 Result 类型,这些错误响应会映射到适当的 HTTP 状态码。


应用结构组织

随着应用规模增长,你会希望将路由拆分到多个文件中。Axum 的 Router 通过 merge 方法让这一点变得非常简单。你可以为应用的不同部分创建独立的路由器,然后将它们合并到一个主路由器中。

例如,你可以创建一个 user_routes.rs 文件:

// user_routes.rs
use axum::{Router, routing::get};

async fn get_users() { /* ... */ }
async fn get_user() { /* ... */ }

pub fn users_router() -> Router {
    Router::new()
        .route("/users", get(get_users))
        .route("/users/:id", get(get_user))  // 路径参数会自动提取
}

然后在主路由器中合并它:

// main.rs
mod user_routes;

fn init_router() -> Router {
    Router::new()
        .route("/", get(hello_world))
        .merge(user_routes::users_router())
    //... with_state, layers, etc.
}

这种方法有助于保持 main.rslib.rs 的整洁,并按功能组织你的应用。


在 Axum 中添加数据库

通常设置数据库时,你需要建立数据库连接:

use axum::{Router, routing::get, extract::State};
use sqlx::{PgPool, PgPoolOptions};
use std::sync::Arc;

// AppState 现在使用 Arc 来持有连接池
struct AppState {
    db: PgPool,
}

#[tokio::main]  // 使用 tokio 作为异步运行时
async fn main() {
    let db_connection_str = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://user:password@localhost/database".to_string());

    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&db_connection_str).await
        .expect("无法连接到数据库");

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

    let app = Router::new()
        .route("/", get(hello_world))
        .with_state(app_state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("监听地址: {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

async fn hello_world() -> &'static str {
    "Hello, world!"
}

你需要自行配置 Postgres 实例——无论是本地安装、通过 Docker 启动,还是其他方式。但使用 Shuttle,你可以省去这一步,因为运行时会为你自动配置数据库:

#[shuttle_runtime::main]
async fn axum(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> shuttle_axum::ShuttleAxum {
    let state = Arc::new(AppState { db: pool });

    // .. 其余代码
}

在本地,这是通过 Docker 完成的;而在部署环境中,有一个统一的流程为你自动完成!无需额外工作。我们还提供 AWS RDS 数据库服务,无需任何 AWS 知识即可设置——点击此处 了解更多信息。


Axum 中的应用状态(App State)

你可能会问:“我该如何存储数据库连接池和其他全局状态变量?我不想每次操作都重新初始化连接池!”——这是一个完全合理的问题,也很容易解答!

你可能注意到之前我们使用 axum::Extension 来存储状态——这在某些场景下是可行的,但缺点是类型安全性不足。在包括 Axum 在内的大多数 Rust Web 框架中,我们使用所谓的“应用状态”(app state)——一个专门用于在应用各路由间共享变量的结构体。

在 Axum 中共享状态的最佳实践是将其包装在 Arc(原子引用计数器)中。这允许多个应用部分安全地并发访问该状态。

use sqlx::PgPool;
use std::sync::Arc;

struct AppState {
    pool: PgPool,
}

#[shuttle_runtime::main]
async fn axum(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> shuttle_axum::ShuttleAxum {
    let state = Arc::new(AppState { pool });

    // .. 其余代码
}

使用时,我们将状态插入路由器,并在函数中通过参数传入:

use axum::{Router, routing::get, extract::State};
use std::sync::Arc;

// 这是你之前的 AppState
struct AppState { /* ... */ }

fn init_router(state: Arc<AppState>) -> Router {
    Router::new()
        .route("/", get(hello_world))
        .route("/do_something", get(do_something))
        .with_state(state)
}

// 注意:添加应用状态不是强制的——仅在需要时才使用
async fn hello_world() -> &'static str {
    "Hello world!"
}

async fn do_something(
    State(state): State<Arc<AppState>>
) -> Result<ApiResponse, ApiError> {
    // .. 你的代码
}

你也可以在状态结构体上 #[derive(Clone)]。Axum 的 with_state 会自动为你将其包装在 Arc 中。不过,显式使用 Arc 通常能让代码更清晰地表明状态是如何被共享的,因此我们推荐这样做。

你还可以从应用状态中派生子状态!这在我们需要主状态中的某些变量,但又希望限制特定路由的访问权限时非常有用。如下所示:

// 应用状态
#[derive(Clone)]
struct AppState {
    // 包含一些 API 特定的状态
    api_state: ApiState,
}

// API 特定的状态
#[derive(Clone)]
struct ApiState {}

// 支持将 `AppState` 转换为 `ApiState`
impl FromRef<AppState> for ApiState {
    fn from_ref(app_state: &AppState) -> ApiState {
        app_state.api_state.clone()
    }
}

Axum 中的提取器(Extractors):路径参数与查询参数

提取器的作用正如其名:它们从传入的请求中提取数据,并通过允许你将它们作为处理器函数的参数来工作。目前,Axum 原生支持提取大量内容,如单独的头部、路径参数、查询参数、表单和 JSON。社区还提供了对 MsgPack、JWT 提取器等的支持!你也可以创建自己的提取器,稍后我们会介绍。

路径参数及其语法

Axum 可轻松从路由中提取路径参数。路径参数语法使用冒号(:)前缀来表示路由路径中的动态段。

提取 JSON 与查询参数

例如,我们可以使用 axum::Json 类型从 HTTP 请求中提取 JSON 请求体。如下所示:

use axum::Json;
use serde_json::Value;

async fn my_function(
    Json(json): Json<Value>
) -> Result<ApiResponse, ApiError> {
    // ... 你的代码
}

然而,使用 serde_json::Value(未结构化的类型,可包含任意内容)可能不够方便!让我们改用一个实现了 serde::Deserialize 的 Rust 结构体(这是将原始数据转换为结构体所必需的):

use axum::Json;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Submission {
    message: String
}

async fn my_function(
    Json(json): Json<Submission>
) -> Result<ApiResponse, ApiError> {
    println!("{}", json.message);

    // ... 你的代码
}

注意:结构体中未包含的字段将被忽略——根据你的用例,这可能是好事;例如,当你接收 webhook 但只关心其中某些字段时。

表单和 URL 查询参数的处理方式相同,只需在处理器函数中添加相应类型即可。Axum 提供了用于解析查询字符串的查询提取器。例如,表单提取器可能如下所示:

async fn my_function(
    Form(form): Form<Submission>
) -> Result<ApiResponse, ApiError> {
    println!("{}", form.message);  // 注意:这里应为 form.message

    // ... 你的代码
}

在 HTML 端向 API 发送 HTTP 请求时,你也需要确保发送了正确的 Content-Type

头部的处理方式也相同,但头部不会消耗请求体——这意味着你可以使用任意多个!我们可以使用 TypedHeader 类型来实现这一点。在 Axum 0.6 中,你需要启用 headers 功能;而在 0.7 中,它已被移至 axum-extra crate,你需要添加 typed-header 功能:

cargo add axum-extra -F typed-header

使用类型化头部非常简单,只需将其作为处理器函数的参数:

// Axum 0.6 使用方式
use axum::{TypedHeader, headers::Origin};
// Axum 0.7+ 使用方式
use axum_extra::{TypedHeader, headers::Origin};

async fn my_function(
    TypedHeader(origin): TypedHeader<Origin>
) -> Result<ApiResponse, ApiError> {
    println!("{}", origin.hostname);

    // ... 你的代码
}

你可以在此处找到 TypedHeader 提取器/响应的文档。

除了 TypedHeaderaxum-extra 还提供了许多其他有用的类型。例如,它有一个 CookieJar 提取器,用于管理 Cookie,并内置了加密安全等附加功能(需要注意的是,不同 Cookie Jar 功能适用于不同需求),还有一个用于 gRPC 的 Protobuf 提取器。你可以在此处找到该库的文档。


Axum 中的自定义提取器

现在你对提取器有了更多了解,可能想知道如何创建自己的提取器。例如,假设你需要一个提取器,能根据请求体是 JSON 还是表单进行解析。我们先设置结构体和处理器函数:

#[derive(Debug, Serialize, Deserialize)]
struct Payload {
    foo: String,
}

async fn handler(JsonOrForm(payload): JsonOrForm<Payload>) {
    dbg!(payload);
}

struct JsonOrForm<T>(T);

现在我们可以为 JsonOrForm 结构体实现 FromRequest<S, B>

在 Axum 0.6 中:

#[async_trait]
impl<S, B, T> FromRequest<S, B> for JsonOrForm<T>
where
    B: Send + 'static,
    S: Send + Sync,
    Json<T>: FromRequest<(), B>,
    Form<T>: FromRequest<(), B>,
    T: 'static,
{
    type Rejection = Response;

    async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
        let content_type_header = req.headers().get(CONTENT_TYPE);
        let content_type = content_type_header.and_then(|value| value.to_str().ok());

        if let Some(content_type) = content_type {
            if content_type.starts_with("application/json") {
                let Json(payload) = req.extract().await.map_err(IntoResponse::into_response)?;
                return Ok(Self(payload));
            }

            if content_type.starts_with("application/x-www-form-urlencoded") {
                let Form(payload) = req.extract().await.map_err(IntoResponse::into_response)?;
                return Ok(Self(payload));
            }
        }

        Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
    }
}

在 Axum 0.7 中,此实现略有修改。axum::body::Body 不再是 hyper::body::Body 的重导出,而是其自有类型——这意味着它不再泛型化,Request 类型将始终使用 axum::body::Body。本质上,我们只需移除 B 泛型:

#[async_trait]
impl<S, T> FromRequest<S> for JsonOrForm<T>
where
    S: Send + Sync,
    Json<T>: FromRequest<()>,
    Form<T>: FromRequest<()>,
    T: 'static,
{
    type Rejection = Response;

    async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
        let content_type_header = req.headers().get(CONTENT_TYPE);
        let content_type = content_type_header.and_then(|value| value.to_str().ok());

        if let Some(content_type) = content_type {
            if content_type.starts_with("application/json") {
                let Json(payload) = req.extract().await.map_err(IntoResponse::into_response)?;
                return Ok(Self(payload));
            }

            if content_type.starts_with("application/x-www-form-urlencoded") {
                let Form(payload) = req.extract().await.map_err(IntoResponse::into_response)?;
                return Ok(Self(payload));
            }
        }

        Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
    }
}

Axum 中的中间件

如前所述,Axum 相较于其他框架的一大优势是与 Tower crate 高度兼容。这意味着我们可以有效地使用任何 Tower 中间件来构建 Rust API!这一自有中间件系统为你提供了极大的灵活性和可复用性。

例如,我们可以添加一个 Tower 中间件来压缩响应:

use tower_http::compression::CompressionLayer;
use axum::{routing::get, Router};

fn init_router() -> Router {
    Router::new().route("/", get(hello_world)).layer(CompressionLayer::new())
}

有许多现成的 Tower 中间件 crate 可直接使用,无需自己编写任何中间件!如果你已在应用中使用 Tower 中间件,这是一种重用现有中间件系统的绝佳方式,兼容性确保了无缝集成。Rust 语言的类型系统还能帮助防止中间件链中的常见问题(如内存泄漏)。

我们也可以通过编写函数来创建自己的中间件。该函数在 Axum 0.6 中需要对 RequestNext 类型使用 <B> 泛型约束(因为 Axum 的 body 类型是泛型的)。示例如下:

use axum::{http::Request, middleware::Next};

async fn check_hello_world<B>(
    req: Request<B>,
    next: Next<B>
) -> Result<Response, StatusCode> {
    // 需要 http crate 来获取头部名称
    if req.headers().get(CONTENT_TYPE).unwrap() != "application/json" {
        return Err(StatusCode::BAD_REQUEST);
    }

    Ok(next.run(req).await)
}

在 Axum 0.7 及更高版本中,你可以移除 <B> 约束,因为 axum::body::Body 类型不再泛型化。这使得 API 更简洁,同时保持了无宏的 API 设计理念:

use axum::{http::Request, middleware::Next};

async fn check_hello_world(
    req: Request,
    next: Next
) -> Result<Response, StatusCode> {
    // 需要 http crate 来获取头部名称
    if req.headers().get(CONTENT_TYPE).unwrap() != "application/json" {
        return Err(StatusCode::BAD_REQUEST);
    }

    Ok(next.run(req).await)
}

要在应用中使用我们创建的新中间件,需使用 Axum 的 axum::middleware::from_fn 函数,它允许我们将函数用作处理器。实际使用如下:

use axum::middleware;

fn init_router() -> Router {
    Router::new().route("/", get(hello_world)).layer(middleware::from_fn(check_hello_world))
}

如果需要在中间件中添加应用状态,可以在处理器函数中添加状态,然后使用 middleware::from_fn_with_state

fn init_router() -> Router {
    let state = setup_state(); // 应用状态初始化放在这里

    Router::new()
        .route("/", get(hello_world))
        .layer(middleware::from_fn_with_state(state.clone(), check_hello_world))
        .with_state(state)
}

在 Axum 中提供静态文件服务

假设你想使用 Axum 提供一些静态文件服务,或者你有一个使用 React 等前端 JavaScript 框架构建的应用,希望将其与 Rust Axum 后端结合,形成一个大型应用,而不是分别托管前端和后端。该如何实现?

Axum 本身不具备此功能,但它与 tower-http 兼容性极强,后者提供了服务于 SPA、Next.js 等框架生成的静态文件,或原始 HTML/CSS/JavaScript 文件的工具。

如果你使用静态生成的文件(假设静态文件位于项目根目录的 dist 文件夹中),可轻松将其加入路由器:

use tower_http::services::ServeDir;

fn init_router() -> Router {
    Router::new()
        .nest_service("/", ServeDir::new("dist"))
}

如果你使用 React、Vue 等 SPA,可将构建后的资源放入相应文件夹,然后使用以下方式:

use tower_http::services::{ServeDir, ServeFile};

fn init_router() -> Router {
    Router::new().nest_service(
         "/", ServeDir::new("dist")
        .not_found_service(ServeFile::new("dist/index.html")),
    )
}

你还可以结合 askamateramaud 等 HTML 模板 crate!这可以与 htmx 等轻量级 JavaScript 库结合,加速产品上线。


测试你的处理器

Axum 设计的一大优势在于其组件(Router、处理器)都是 tower::Service。这意味着你可以在不运行实际 HTTP 服务器的情况下测试它们。tower::ServiceExt trait 提供了 oneshot 方法,可向你的服务发送单个路由请求,使 Rust 中的测试变得简单直接。

以下是测试处理器的方法:

use axum::{
    body::Body,
    http::{Request, StatusCode},
    routing::get,
    Router,
};
use http_body_util::BodyExt; // 用于 `to_bytes`
use tower::ServiceExt; // 用于 `oneshot`

// 用于测试的路由器
fn app() -> Router {
    Router::new().route("/", get(|| async { "Hello, World!" }))
}

#[tokio::test]  // 在测试中使用 tokio 的异步 fn main 模式
async fn test_hello_world() {
    let app = app();

    // `Router` 实现了 `tower::Service<Request<Body>>`,因此我们可以
    // 像调用任何 tower 服务一样调用它,无需运行 HTTP 服务器。
    let response = app
        .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);

    let body = response.into_body().collect().await.unwrap().to_bytes();
    assert_eq!(&body[..], b"Hello, World!");
}

这种方法快速、可靠,能直接测试应用逻辑。你可以构造任意 http::Request 来测试不同场景,包括头部、请求体等。要使其正常工作,你需要在 [dev-dependencies] 中添加带 full 功能的 http-body-util


超越 REST:WebSocket 与 OpenAPI

虽然 Axum 非常适合 REST API,但其能力不止于此。

WebSocket

Axum 对 WebSocket 提供一流支持。你可以使用 axum::extract::ws::WebSocketUpgrade 提取器添加 WebSocket 处理器。该提取器会处理 WebSocket 握手并升级连接,为你提供一个 WebSocket 流来发送和接收消息。

use axum::{
    extract::ws::{WebSocket, WebSocketUpgrade},
    response::IntoResponse,
};

async fn websocket_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
    ws.on_upgrade(handle_socket)
}

async fn handle_socket(mut socket: WebSocket) {
    while let Some(msg) = socket.recv().await {
        let msg = if let Ok(msg) = msg {
            msg
        } else {
            // 客户端断开连接
            return;
        };

        if socket.send(msg).await.is_err() {
            // 客户端断开连接
            return;
        }
    }
}

OpenAPI

对于构建有文档且可维护的 API,OpenAPI(原 Swagger)是行业标准。虽然 Axum 本身不内置 OpenAPI 生成,但 utoipa crate 提供了出色的集成。它允许你使用过程宏从 Axum 处理器和数据类型生成 OpenAPI 规范。