Joshua Mo 2025-07-04
在 Rust Web 生态系统中,有众多后端 Web 框架可供选择,让人难以抉择。在过去,你可能会看到 Rocket 在流行度排行榜上遥遥领先;而如今,通常是 Axum 与 Actix 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::State与std::sync::Arc安全地共享状态(如数据库连接池)。 - 中间件(Middleware):充分利用整个
tower和tower-http生态系统,实现强大且可复用的中间件。 - 测试(Testing):使用
tower::ServiceExt直接高效地测试处理器,无需启动服务器。
本文将全面介绍如何使用 Axum 编写 Web 服务。内容已更新至 Axum 0.8 和 Tokio 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.rs 或 lib.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 提取器/响应的文档。
除了 TypedHeader,axum-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 中需要对 Request 和 Next 类型使用 <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")),
)
}
你还可以结合 askama、tera 和 maud 等 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 规范。