Axum 是 Tokio 生态里很受欢迎的 Web 框架,类型安全、性能好,和 tower 中间件配合也很自然。本文用一个最小可运行的 API 项目,说明日常开发中最常用的几个概念。

最小项目结构

src/
main.rs      # 入口,组装路由
routes/
mod.rs     # 路由注册
handlers.rs
state.rs     # 共享状态

Cargo.toml 核心依赖:

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

第一个 Handler

use axum::{routing::get, Json, Router};
use serde_json::{json, Value};

async fn health() -> Json<Value> {
Json(json!({ "status": "ok" }))
}

#[tokio::main]
async fn main() {
let app = Router::new().route("/health", get(health));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}

访问 GET /health 应返回 JSON。生产环境记得把绑定地址和端口做成配置项。

共享状态:AppState

多个 Handler 需要访问数据库、配置或缓存时,用 Arc 包一层状态:

use std::sync::Arc;
use axum::extract::State;

#[derive(Clone)]
struct AppState {
db: Arc<DbPool>,
}

async fn list_items(State(st): State<AppState>) -> Json<Value> {
let rows = st.db.query_all().await;
Json(json!({ "items": rows }))
}

let state = AppState { db: Arc::new(pool) };
let app = Router::new()
.route("/items", get(list_items))
.with_state(state);

State 提取器会在每个请求里克隆一份 AppState(内部通常是 Arc,克隆成本低)。

路径参数与查询参数

use axum::extract::{Path, Query};
use serde::Deserialize;

async fn get_post(Path(slug): Path<String>) -> Json<Value> {
Json(json!({ "slug": slug }))
}

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

async fn search(Query(q): Query<PageQuery>) -> Json<Value> {
Json(json!({ "page": q.page.unwrap_or(1), "q": q.q }))
}

路径参数用 Path,查询字符串用 Query + serde 反序列化,和写普通结构体一样直观。

统一错误响应

不要把 unwrap 散落在 Handler 里。可以定义业务错误类型,实现 IntoResponse

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

enum ApiError {
NotFound,
BadRequest(String),
}

impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, msg) = match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "资源不存在"),
ApiError::BadRequest(ref s) => (StatusCode::BAD_REQUEST, s.as_str()),
};
(status, Json(json!({ "status": "error", "message": msg }))).into_response()
}
}

Handler 返回 Result<Json<Value>, ApiError>,成功走 Ok,失败自动转成对应 HTTP 状态码。

中间件:鉴权与日志

Axum 基于 Tower,常用写法:

use axum::middleware;
use tower_http::trace::TraceLayer;

let app = Router::new()
.route("/public", get(public_handler))
.nest(
"/admin",
Router::new()
.route("/posts", get(admin_posts))
.layer(middleware::from_fn(require_auth)),
)
.layer(TraceLayer::new_for_http());

公开路由和需登录路由用 nest 分组,中间件只挂在需要的子树上,结构清晰。

小结

场景推荐做法
JSON APIJson<T> + serde
共享资源State<AppState> + Arc
错误处理自定义 IntoResponse
鉴权 / 日志tower 中间件分层挂载

Axum 的学习曲线比脚本语言框架陡一点,但编译期能拦住大量低级错误。个人爬虫、博客、小工具的后端,用 Rust + Axum 往往比「Python 脚本 + 临时 HTTP 库」更省心——尤其是要长期挂着跑的时候。