不灭的焱

革命尚未成功,同志仍须努力 Trae下载

作者:AlbertWen  添加时间:2026-08-01 00:18:10  修改时间:2026-08-23 21:04:24  分类:01.Rust编程  编辑

目录

Axum 详细使用教程

本文基于 Axum 0.8.9tower-http 0.7.x 和 Rust 2024 Edition。Axum 是 Tokio 生态中的 HTTP 路由和请求处理库,核心特点是:通过 Router 配置路由、通过 Extractor 提取请求参数、通过 IntoResponse 生成响应,并直接使用 Tower/Tower HTTP 的中间件体系。(文档.rs)

一、Axum 的核心概念

可以把 Axum 和 Java Spring Boot 做如下对应:

Axum Spring Boot 作用
Router @RequestMapping 定义路由
Handler 函数 Controller 方法 处理请求
Path @PathVariable 路径参数
Query @RequestParam 查询参数
Json<T> @RequestBody JSON 请求体
State<T> 依赖注入 数据库、Redis、配置等
IntoResponse ResponseEntity 构造响应
Layer Filter / Interceptor 日志、鉴权、CORS
Router::nest Controller 路径前缀 模块化路由

一个请求的处理过程大致如下:

HTTP 请求
   ↓
TcpListener
   ↓
Router 路由匹配
   ↓
Middleware 中间件
   ↓
Extractor 参数提取
   ↓
Handler 处理函数
   ↓
IntoResponse 转换
   ↓
HTTP 响应

Axum 的 Handler 本质上是一个异步函数:参数是一个或多个 Extractor,返回值需要实现 IntoResponse。(文档.rs)

二、创建第一个 Axum 项目

1. 创建项目

cargo new axum-demo
cd axum-demo

2. 配置 Cargo.toml

[package]
name = "axum-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.8"

tokio = {
    version = "1",
    features = ["full"]
}

serde = {
    version = "1",
    features = ["derive"]
}

serde_json = "1"
thiserror = "2"

tower-http = {
    version = "0.7",
    features = ["cors", "trace"]
}

tracing = "0.1"

tracing-subscriber = {
    version = "0.3",
    features = ["env-filter"]
}

[dev-dependencies]
tower = {
    version = "0.5",
    features = ["util"]
}

三、最简单的 Hello World

修改 src/main.rs

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

#[tokio::main]
async fn main() {
    // 创建路由
    let app = Router::new()
        .route("/", get(index));

    // 绑定端口
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .expect("端口绑定失败");

    println!("服务器启动:http://127.0.0.1:3000");

    // 启动服务
    axum::serve(listener, app)
        .await
        .expect("服务器运行失败");
}

async fn index() -> &'static str {
    "Hello Axum"
}

运行:

cargo run

访问:

http://127.0.0.1:3000

Axum 官方推荐使用 tokio::net::TcpListener 绑定地址,再交给 axum::serve 启动服务。(文档.rs)

四、路由的基本用法

1. 不同 HTTP 方法

use axum::{
    Router,
    routing::{delete, get, post, put},
};

fn create_router() -> Router {
    Router::new()
        .route("/users", get(list_users))
        .route("/users", post(create_user))
        .route("/users/{id}", get(get_user))
        .route("/users/{id}", put(update_user))
        .route("/users/{id}", delete(delete_user))
}

async fn list_users() -> &'static str {
    "查询用户列表"
}

async fn create_user() -> &'static str {
    "创建用户"
}

async fn get_user() -> &'static str {
    "查询用户详情"
}

async fn update_user() -> &'static str {
    "更新用户"
}

async fn delete_user() -> &'static str {
    "删除用户"
}

也可以把同一路径的不同请求方法写在一起:

Router::new()
    .route(
        "/users",
        get(list_users).post(create_user),
    )
    .route(
        "/users/{id}",
        get(get_user)
            .put(update_user)
            .delete(delete_user),
    )

Axum 0.8 的路径参数使用:

/users/{id}

通配符使用:

/assets/{*path}

旧版本教程中常见的 /users/:id 不应继续用于 Axum 0.8。(文档.rs)

2. 路由分组

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

fn user_routes() -> Router {
    Router::new()
        .route("/", get(list_users))
        .route("/{id}", get(get_user))
}

fn system_routes() -> Router {
    Router::new()
        .route("/health", get(health))
        .route("/version", get(version))
}

fn create_router() -> Router {
    Router::new()
        .nest("/api/users", user_routes())
        .nest("/api/system", system_routes())
}

async fn list_users() -> &'static str {
    "用户列表"
}

async fn get_user() -> &'static str {
    "用户详情"
}

async fn health() -> &'static str {
    "OK"
}

async fn version() -> &'static str {
    "1.0.0"
}

最终访问路径为:

GET /api/users
GET /api/users/1001

GET /api/system/health
GET /api/system/version

Router::nest 适合将大型项目拆分成多个业务模块。(文档.rs)

五、请求参数提取 Extractor

Extractor 是 Axum 最核心的设计之一。

常见 Extractor 包括:

Path<T>
Query<T>
Json<T>
State<T>
HeaderMap
Method
Uri
Extension<T>
Request

Extractor 会按照 Handler 参数从左到右执行。请求体是只能消费一次的异步数据流,因此 JsonStringBytesRequest 等会消费请求体的 Extractor,必须放在 Handler 参数最后。(文档.rs)

1. Path:路径参数

请求地址:

GET /users/1001

代码:

use axum::extract::Path;

async fn get_user(
    Path(id): Path<u64>,
) -> String {
    format!("用户ID:{id}")
}

路由:

Router::new()
    .route("/users/{id}", get(get_user))

2. 提取多个路径参数

请求:

GET /departments/10/users/1001

代码:

use axum::extract::Path;

async fn get_department_user(
    Path((department_id, user_id)): Path<(u64, u64)>,
) -> String {
    format!(
        "部门ID:{},用户ID:{}",
        department_id,
        user_id
    )
}

路由:

Router::new().route(
    "/departments/{department_id}/users/{user_id}",
    get(get_department_user),
)

也可以使用结构体:

use axum::extract::Path;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct UserPath {
    department_id: u64,
    user_id: u64,
}

async fn get_department_user(
    Path(path): Path<UserPath>,
) -> String {
    format!(
        "部门ID:{},用户ID:{}",
        path.department_id,
        path.user_id
    )
}

结构体字段名称必须与路由参数名称对应。

3. Query:查询参数

请求:

GET /users?page=1&page_size=20&keyword=admin

代码:

use axum::extract::Query;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct UserQuery {
    page: Option<u64>,
    page_size: Option<u64>,
    keyword: Option<String>,
}

async fn list_users(
    Query(query): Query<UserQuery>,
) -> String {
    let page = query.page.unwrap_or(1);
    let page_size = query.page_size.unwrap_or(20);
    let keyword = query.keyword.unwrap_or_default();

    format!(
        "page={}, page_size={}, keyword={}",
        page,
        page_size,
        keyword
    )
}

4. Json:JSON 请求体

请求:

POST /users
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com"
}

代码:

use axum::Json;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
struct CreateUserReq {
    name: String,
    email: String,
}

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

async fn create_user(
    Json(payload): Json<CreateUserReq>,
) -> Json<UserResponse> {
    Json(UserResponse {
        id: 1001,
        name: payload.name,
        email: payload.email,
    })
}

注意 Handler 参数顺序:

async fn create_user(
    State(state): State<AppState>,
    Path(id): Path<u64>,
    headers: HeaderMap,
    Json(payload): Json<CreateUserReq>,
)

Json 要放在最后,因为它需要读取请求体。

六、响应结果

Axum Handler 可以返回任何实现了 IntoResponse 的类型,包括字符串、状态码、JSON、元组以及自定义类型。(文档.rs)

1. 返回字符串

async fn index() -> &'static str {
    "Hello Axum"
}

默认状态码:

200 OK

2. 返回状态码和字符串

use axum::http::StatusCode;

async fn create_user() -> (StatusCode, &'static str) {
    (
        StatusCode::CREATED,
        "用户创建成功",
    )
}

3. 返回 JSON

use axum::Json;
use serde::Serialize;

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

async fn get_user() -> Json<User> {
    Json(User {
        id: 1001,
        name: "张三".to_string(),
    })
}

响应:

{
  "id": 1001,
  "name": "张三"
}

4. 统一响应结构

建议业务系统统一返回:

{
  "code": 0,
  "message": "成功",
  "data": {}
}

定义:

use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
    pub code: i32,
    pub message: String,
    pub data: Option<T>,
}

impl<T> ApiResponse<T> {
    pub fn success(data: T) -> Self {
        Self {
            code: 0,
            message: "成功".to_string(),
            data: Some(data),
        }
    }
}

impl ApiResponse<()> {
    pub fn success_empty() -> Self {
        Self {
            code: 0,
            message: "成功".to_string(),
            data: None,
        }
    }

    pub fn error(
        code: i32,
        message: impl Into<String>,
    ) -> Self {
        Self {
            code,
            message: message.into(),
            data: None,
        }
    }
}

使用:

async fn get_user() -> Json<ApiResponse<User>> {
    let user = User {
        id: 1001,
        name: "张三".to_string(),
    };

    Json(ApiResponse::success(user))
}

七、完整 CRUD 示例

下面实现一个完整的用户管理 API。

为了集中展示 Axum 的用法,暂时使用内存 HashMap 保存数据。实际项目中可以替换为 SeaORM 的数据库访问层。

src/main.rs

use std::{
    collections::HashMap,
    sync::{
        Arc,
        atomic::{
            AtomicU64,
            Ordering,
        },
    },
};

use axum::{
    Json,
    Router,
    extract::{
        Path,
        Query,
        Request,
        State,
    },
    http::{
        Method,
        StatusCode,
        header::{
            AUTHORIZATION,
            CONTENT_TYPE,
        },
    },
    middleware::{
        self,
        Next,
    },
    response::{
        IntoResponse,
        Response,
    },
    routing::get,
};

use serde::{
    Deserialize,
    Serialize,
};

use thiserror::Error;
use tokio::sync::RwLock;

use tower_http::{
    cors::{
        Any,
        CorsLayer,
    },
    trace::TraceLayer,
};

use tracing_subscriber::EnvFilter;

// ==========================================================
// 应用状态
// ==========================================================

struct AppState {
    users: RwLock<HashMap<u64, User>>,
    next_id: AtomicU64,
}

impl AppState {
    fn new() -> Self {
        Self {
            users: RwLock::new(HashMap::new()),
            next_id: AtomicU64::new(1),
        }
    }
}

// ==========================================================
// 数据模型
// ==========================================================

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

// ==========================================================
// 请求对象
// ==========================================================

#[derive(Debug, Deserialize)]
struct CreateUserReq {
    name: String,
    email: String,
}

#[derive(Debug, Deserialize)]
struct UpdateUserReq {
    name: Option<String>,
    email: Option<String>,
}

#[derive(Debug, Deserialize)]
struct PageQuery {
    page: Option<u64>,
    page_size: Option<u64>,
}

// ==========================================================
// 响应对象
// ==========================================================

#[derive(Debug, Serialize)]
struct PageResult<T> {
    items: Vec<T>,
    total: u64,
    page: u64,
    page_size: u64,
}

#[derive(Debug, Serialize)]
struct ApiResponse<T> {
    code: i32,
    message: String,
    data: Option<T>,
}

impl<T> ApiResponse<T> {
    fn success(data: T) -> Self {
        Self {
            code: 0,
            message: "成功".to_string(),
            data: Some(data),
        }
    }
}

impl ApiResponse<()> {
    fn success_empty() -> Self {
        Self {
            code: 0,
            message: "成功".to_string(),
            data: None,
        }
    }

    fn error(
        code: i32,
        message: impl Into<String>,
    ) -> Self {
        Self {
            code,
            message: message.into(),
            data: None,
        }
    }
}

// ==========================================================
// 统一异常处理
// ==========================================================

#[derive(Debug, Error)]
enum AppError {
    #[error("请求参数错误:{0}")]
    BadRequest(String),

    #[error("资源不存在:{0}")]
    NotFound(String),

    #[error("未登录或访问令牌无效")]
    Unauthorized,

    #[error("服务器内部错误")]
    Internal,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let message = self.to_string();

        let (status, code) = match self {
            AppError::BadRequest(_) => {
                (StatusCode::BAD_REQUEST, 40001)
            }
            AppError::NotFound(_) => {
                (StatusCode::NOT_FOUND, 40401)
            }
            AppError::Unauthorized => {
                (StatusCode::UNAUTHORIZED, 40101)
            }
            AppError::Internal => {
                (StatusCode::INTERNAL_SERVER_ERROR, 50001)
            }
        };

        (
            status,
            Json(ApiResponse::<()>::error(code, message)),
        )
            .into_response()
    }
}

// ==========================================================
// Handler
// ==========================================================

async fn health() -> Json<ApiResponse<&'static str>> {
    Json(ApiResponse::success("UP"))
}

async fn list_users(
    State(state): State<Arc<AppState>>,
    Query(query): Query<PageQuery>,
) -> Json<ApiResponse<PageResult<User>>> {
    let page = query.page.unwrap_or(1).max(1);
    let page_size = query.page_size.unwrap_or(20).clamp(1, 100);

    let users = state.users.read().await;

    let total = users.len() as u64;

    let mut items: Vec<User> =
        users.values().cloned().collect();

    items.sort_by_key(|user| user.id);

    let offset = ((page - 1) * page_size) as usize;

    let items = items
        .into_iter()
        .skip(offset)
        .take(page_size as usize)
        .collect();

    Json(ApiResponse::success(PageResult {
        items,
        total,
        page,
        page_size,
    }))
}

async fn get_user(
    State(state): State<Arc<AppState>>,
    Path(id): Path<u64>,
) -> Result<Json<ApiResponse<User>>, AppError> {
    let users = state.users.read().await;

    let user = users
        .get(&id)
        .cloned()
        .ok_or_else(|| {
            AppError::NotFound(format!("用户ID={id}"))
        })?;

    Ok(Json(ApiResponse::success(user)))
}

async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<CreateUserReq>,
) -> Result<
    (StatusCode, Json<ApiResponse<User>>),
    AppError,
> {
    let CreateUserReq {
        name,
        email,
    } = payload;

    let name = name.trim().to_string();
    let email = email.trim().to_lowercase();

    if name.is_empty() {
        return Err(AppError::BadRequest(
            "用户名称不能为空".to_string(),
        ));
    }

    // 演示用的简单邮箱校验
    if !email.contains('@') {
        return Err(AppError::BadRequest(
            "邮箱格式不正确".to_string(),
        ));
    }

    let mut users = state.users.write().await;

    let email_exists = users
        .values()
        .any(|user| {
            user.email.eq_ignore_ascii_case(&email)
        });

    if email_exists {
        return Err(AppError::BadRequest(
            "邮箱已经存在".to_string(),
        ));
    }

    let id = state.next_id.fetch_add(
        1,
        Ordering::Relaxed,
    );

    let user = User {
        id,
        name,
        email,
    };

    users.insert(id, user.clone());

    Ok((
        StatusCode::CREATED,
        Json(ApiResponse::success(user)),
    ))
}

async fn update_user(
    State(state): State<Arc<AppState>>,
    Path(id): Path<u64>,
    Json(payload): Json<UpdateUserReq>,
) -> Result<Json<ApiResponse<User>>, AppError> {
    let UpdateUserReq {
        name,
        email,
    } = payload;

    let mut users = state.users.write().await;

    if !users.contains_key(&id) {
        return Err(AppError::NotFound(
            format!("用户ID={id}"),
        ));
    }

    // 修改邮箱前,先检查是否与其他用户冲突
    if let Some(email_value) = email.as_ref() {
        let new_email = email_value.trim();

        if !new_email.contains('@') {
            return Err(AppError::BadRequest(
                "邮箱格式不正确".to_string(),
            ));
        }

        let email_exists = users.values().any(|user| {
            user.id != id
                && user
                    .email
                    .eq_ignore_ascii_case(new_email)
        });

        if email_exists {
            return Err(AppError::BadRequest(
                "邮箱已经存在".to_string(),
            ));
        }
    }

    let user = users
        .get_mut(&id)
        .expect("用户存在性已经检查");

    if let Some(name_value) = name {
        let name_value = name_value.trim();

        if name_value.is_empty() {
            return Err(AppError::BadRequest(
                "用户名称不能为空".to_string(),
            ));
        }

        user.name = name_value.to_string();
    }

    if let Some(email_value) = email {
        user.email = email_value.trim().to_lowercase();
    }

    Ok(Json(ApiResponse::success(user.clone())))
}

async fn delete_user(
    State(state): State<Arc<AppState>>,
    Path(id): Path<u64>,
) -> Result<Json<ApiResponse<()>>, AppError> {
    let mut users = state.users.write().await;

    users
        .remove(&id)
        .ok_or_else(|| {
            AppError::NotFound(format!("用户ID={id}"))
        })?;

    Ok(Json(ApiResponse::success_empty()))
}

// ==========================================================
// 鉴权中间件
// ==========================================================

async fn auth_middleware(
    request: Request,
    next: Next,
) -> Result<Response, AppError> {
    let token = request
        .headers()
        .get(AUTHORIZATION)
        .and_then(|value| value.to_str().ok());

    if token != Some("Bearer dev-token") {
        return Err(AppError::Unauthorized);
    }

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

// ==========================================================
// 404
// ==========================================================

async fn not_found() -> impl IntoResponse {
    AppError::NotFound("请求路径".to_string())
}

// ==========================================================
// 创建路由
// ==========================================================

fn build_app(
    state: Arc<AppState>,
) -> Router {
    let api_routes = Router::new()
        .route(
            "/users",
            get(list_users).post(create_user),
        )
        .route(
            "/users/{id}",
            get(get_user)
                .put(update_user)
                .delete(delete_user),
        )
        // 只对 /api 下的接口进行认证
        .route_layer(
            middleware::from_fn(auth_middleware),
        );

    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods([
            Method::GET,
            Method::POST,
            Method::PUT,
            Method::DELETE,
        ])
        .allow_headers([
            CONTENT_TYPE,
            AUTHORIZATION,
        ]);

    Router::new()
        .route("/health", get(health))
        .nest("/api", api_routes)
        .fallback(not_found)
        .with_state(state)
        .layer(cors)
        .layer(TraceLayer::new_for_http())
}

// ==========================================================
// 优雅停机
// ==========================================================

async fn shutdown_signal() {
    tokio::signal::ctrl_c()
        .await
        .expect("监听 Ctrl+C 失败");

    tracing::info!("收到停机信号");
}

// ==========================================================
// 程序入口
// ==========================================================

#[tokio::main]
async fn main() {
    let filter =
        EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| {
                "axum_demo=debug,tower_http=info".into()
            });

    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .init();

    let state = Arc::new(AppState::new());

    let app = build_app(state);

    let listener =
        tokio::net::TcpListener::bind(
            "0.0.0.0:3000",
        )
        .await
        .expect("端口绑定失败");

    tracing::info!(
        "服务启动:http://127.0.0.1:3000"
    );

    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .expect("服务器运行失败");
}

八、测试 CRUD 接口

1. 健康检查

健康检查没有配置鉴权:

curl http://127.0.0.1:3000/health

返回:

{
  "code": 0,
  "message": "成功",
  "data": "UP"
}

2. 创建用户

curl -X POST http://127.0.0.1:3000/api/users \
  -H "Authorization: Bearer dev-token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "张三",
    "email": "zhangsan@example.com"
  }'

返回:

{
  "code": 0,
  "message": "成功",
  "data": {
    "id": 1,
    "name": "张三",
    "email": "zhangsan@example.com"
  }
}

3. 查询用户列表

curl "http://127.0.0.1:3000/api/users?page=1&page_size=20" \
  -H "Authorization: Bearer dev-token"

返回:

{
  "code": 0,
  "message": "成功",
  "data": {
    "items": [
      {
        "id": 1,
        "name": "张三",
        "email": "zhangsan@example.com"
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20
  }
}

4. 查询单个用户

curl http://127.0.0.1:3000/api/users/1 \
  -H "Authorization: Bearer dev-token"

5. 修改用户

curl -X PUT http://127.0.0.1:3000/api/users/1 \
  -H "Authorization: Bearer dev-token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "张三丰"
  }'

6. 删除用户

curl -X DELETE http://127.0.0.1:3000/api/users/1 \
  -H "Authorization: Bearer dev-token"

7. 不携带 Token

curl http://127.0.0.1:3000/api/users

返回状态码:

401 Unauthorized

返回内容:

{
  "code": 40101,
  "message": "未登录或访问令牌无效",
  "data": null
}

九、State 状态管理

State 一般用于保存:

数据库连接池
Redis 客户端
系统配置
HTTP 客户端
消息队列客户端
业务 Service

例如:

#[derive(Clone)]
struct AppState {
    db: DatabaseConnection,
    redis: RedisClient,
    config: Arc<AppConfig>,
}

注册:

let state = AppState {
    db,
    redis,
    config: Arc::new(config),
};

let app = Router::new()
    .route("/users", get(list_users))
    .with_state(state);

提取:

async fn list_users(
    State(state): State<AppState>,
) {
    // state.db
    // state.redis
    // state.config
}

Axum 会为每次请求克隆 State,因此 State 必须实现 Clone。如果 State 内部包含较重或不可直接克隆的数据,可以使用 Arc<AppState>;官方文档也建议通过 Arc 降低每次请求克隆状态的成本。(文档.rs)

数据库连接池通常自身已经是共享句柄,因此也可以直接放入可克隆的 AppState 中。

十、中间件

Axum 不单独发明一套中间件协议,而是使用 Tower 的 ServiceLayer 体系。普通项目可使用 middleware::from_fn 编写异步中间件;日志、CORS、超时、压缩等通用能力通常使用 tower-http。(文档.rs)

1. 自定义日志中间件

use std::time::Instant;

use axum::{
    extract::Request,
    middleware::Next,
    response::Response,
};

async fn request_log(
    request: Request,
    next: Next,
) -> Response {
    let method = request.method().clone();
    let uri = request.uri().clone();

    let start = Instant::now();

    let response = next.run(request).await;

    let elapsed = start.elapsed();

    tracing::info!(
        method = %method,
        uri = %uri,
        status = %response.status(),
        elapsed_ms = elapsed.as_millis(),
        "HTTP请求完成"
    );

    response
}

注册:

Router::new()
    .route("/", get(index))
    .layer(
        middleware::from_fn(request_log),
    )

2. TraceLayer

use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(index))
    .layer(
        TraceLayer::new_for_http(),
    );

TraceLayer 会记录请求方法、路径、响应状态和耗时等信息。(文档.rs)

3. CORS

开发环境:

use tower_http::cors::{
    Any,
    CorsLayer,
};

let cors = CorsLayer::new()
    .allow_origin(Any)
    .allow_methods(Any)
    .allow_headers(Any);

生产环境不要无条件允许任意来源,应该指定前端域名,例如:

use axum::{
    http::{
        HeaderValue,
        Method,
        header::{
            AUTHORIZATION,
            CONTENT_TYPE,
        },
    },
};

let cors = CorsLayer::new()
    .allow_origin(
        "https://admin.example.com"
            .parse::<HeaderValue>()
            .expect("前端地址不正确"),
    )
    .allow_methods([
        Method::GET,
        Method::POST,
        Method::PUT,
        Method::DELETE,
    ])
    .allow_headers([
        CONTENT_TYPE,
        AUTHORIZATION,
    ]);

CorsLayer 负责向响应中添加对应的 CORS Header。(文档.rs)

十一、统一错误处理

推荐 Handler 返回:

Result<T, AppError>

例如:

async fn get_user(
    Path(id): Path<u64>,
) -> Result<Json<User>, AppError> {
    let user = find_user(id)
        .await
        .ok_or_else(|| {
            AppError::NotFound(
                format!("用户ID={id}"),
            )
        })?;

    Ok(Json(user))
}

关键是为 AppError 实现:

IntoResponse

这样 ? 可以一路把错误返回给 Axum,最终转换成统一 HTTP 响应。

对于数据库错误,可以继续扩展:

#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("数据库操作失败:{0}")]
    Database(String),

    #[error("资源不存在:{0}")]
    NotFound(String),

    #[error("请求参数错误:{0}")]
    BadRequest(String),
}

或者使用 #[from] 自动转换:

#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("数据库操作失败:{0}")]
    Database(
        #[from]
        sea_orm::DbErr,
    ),
}

之后:

let users = UserEntity::find()
    .all(&state.db)
    .await?;

DbErr 会自动通过 From<DbErr> 转换为 AppError

十二、生产项目目录结构

根据你之前希望采用的 Java 分层思想,Axum 项目可以这样设计:

src/
├── main.rs
├── app.rs
├── state.rs
├── config.rs
├── error.rs
│
├── common/
│   ├── mod.rs
│   ├── response.rs
│   ├── pagination.rs
│   └── middleware/
│       ├── mod.rs
│       ├── auth.rs
│       └── request_log.rs
│
├── modules/
│   ├── mod.rs
│   │
│   ├── user/
│   │   ├── mod.rs
│   │   ├── router.rs
│   │   ├── handler.rs
│   │   ├── service.rs
│   │   ├── repository.rs
│   │   ├── entity.rs
│   │   └── dto/
│   │       ├── mod.rs
│   │       ├── create_user_req.rs
│   │       ├── update_user_req.rs
│   │       └── user_response.rs
│   │
│   └── auth/
│       ├── mod.rs
│       ├── router.rs
│       ├── handler.rs
│       ├── service.rs
│       ├── repository.rs
│       └── dto/
│
└── infrastructure/
    ├── mod.rs
    ├── database.rs
    ├── redis.rs
    └── tracing.rs

职责划分:

router.rs
    只负责路径、HTTP 方法、中间件绑定

handler.rs
    接收 HTTP 参数,调用 Service,转换响应

service.rs
    业务逻辑、事务、权限规则

repository.rs
    数据访问、SeaORM 查询

entity.rs
    数据库实体

dto/
    Req、Resp、DTO 对象

state.rs
    数据库、Redis、配置等共享状态

error.rs
    统一错误类型

路由层示例

use std::sync::Arc;

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

use crate::{
    modules::user::handler,
    state::AppState,
};

pub fn routes() -> Router<Arc<AppState>> {
    Router::new()
        .route(
            "/",
            get(handler::list)
                .post(handler::create),
        )
        .route(
            "/{id}",
            get(handler::detail)
                .put(handler::update)
                .delete(handler::delete),
        )
}

Handler 层示例

pub async fn detail(
    State(state): State<Arc<AppState>>,
    Path(id): Path<u64>,
) -> Result<
    Json<ApiResponse<UserResponse>>,
    AppError,
> {
    let user =
        user_service::find_by_id(
            &state,
            id,
        )
        .await?;

    Ok(Json(ApiResponse::success(user)))
}

Service 层示例

pub async fn find_by_id(
    state: &AppState,
    id: u64,
) -> Result<UserResponse, AppError> {
    let user =
        user_repository::find_by_id(
            &state.db,
            id,
        )
        .await?
        .ok_or_else(|| {
            AppError::NotFound(
                format!("用户ID={id}"),
            )
        })?;

    Ok(UserResponse::from(user))
}

Repository 层示例

pub async fn find_by_id(
    db: &DatabaseConnection,
    id: u64,
) -> Result<Option<UserModel>, DbErr> {
    UserEntity::find_by_id(id)
        .one(db)
        .await
}

调用关系:

Router
   ↓
Handler
   ↓
Service
   ↓
Repository
   ↓
SeaORM
   ↓
MySQL

十三、接口测试

Axum 的 Router 实现了 Tower Service,所以测试时不必真正启动 HTTP 服务器,可以直接通过 oneshot 发送请求。官方测试示例也采用这一方式。(GitHub)

将下面代码放到 main.rs 最后:

#[cfg(test)]
mod tests {
    use super::*;

    use axum::{
        body::Body,
        http::{
            Request,
            StatusCode,
        },
    };

    use tower::ServiceExt;

    #[tokio::test]
    async fn health_should_return_200() {
        let state =
            Arc::new(AppState::new());

        let app = build_app(state);

        let request = Request::builder()
            .uri("/health")
            .body(Body::empty())
            .expect("创建请求失败");

        let response = app
            .oneshot(request)
            .await
            .expect("请求执行失败");

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

    #[tokio::test]
    async fn users_should_require_token() {
        let state =
            Arc::new(AppState::new());

        let app = build_app(state);

        let request = Request::builder()
            .uri("/api/users")
            .body(Body::empty())
            .expect("创建请求失败");

        let response = app
            .oneshot(request)
            .await
            .expect("请求执行失败");

        assert_eq!(
            response.status(),
            StatusCode::UNAUTHORIZED,
        );
    }

    #[tokio::test]
    async fn users_should_allow_valid_token() {
        let state =
            Arc::new(AppState::new());

        let app = build_app(state);

        let request = Request::builder()
            .uri("/api/users")
            .header(
                AUTHORIZATION,
                "Bearer dev-token",
            )
            .body(Body::empty())
            .expect("创建请求失败");

        let response = app
            .oneshot(request)
            .await
            .expect("请求执行失败");

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

运行测试:

cargo test

十四、常见错误

1. Handler trait is not implemented

常见错误:

the trait Handler<_, _> is not implemented

原因之一是 Json 没有放在最后:

// 错误
async fn create(
    Json(req): Json<CreateUserReq>,
    State(state): State<AppState>,
) {
}

正确:

async fn create(
    State(state): State<AppState>,
    Json(req): Json<CreateUserReq>,
) {
}

2. State 没有实现 Clone

错误:

the trait Clone is not implemented for AppState

解决方法一:

#[derive(Clone)]
struct AppState {
    db: DatabaseConnection,
}

解决方法二:

let state = Arc::new(AppState {
    // ...
});

Handler:

State(state): State<Arc<AppState>>

3. 路由参数写成旧格式

不推荐:

.route("/users/:id", get(get_user))

Axum 0.8 应使用:

.route("/users/{id}", get(get_user))

4. impl IntoResponse 分支类型不同

下面可能无法编译:

async fn handler() -> impl IntoResponse {
    if condition {
        Json(data)
    } else {
        StatusCode::NOT_FOUND
    }
}

因为两个分支返回的是不同具体类型。

可以统一转成 Response

async fn handler() -> Response {
    if condition {
        Json(data).into_response()
    } else {
        StatusCode::NOT_FOUND.into_response()
    }
}

更推荐使用:

async fn handler()
    -> Result<Json<Data>, AppError>
{
    // ...
}

十五、学习 Axum 时应重点掌握什么

建议按照以下顺序学习:

1. Router 和 HTTP 方法
2. Handler 异步函数
3. Path、Query、Json
4. IntoResponse
5. Result<T, AppError>
6. State 状态共享
7. Tower 中间件
8. 模块化 Router
9. SeaORM 数据库访问
10. JWT、RBAC 和接口测试

对于你的技术栈,推荐最终组合为:

Axum
Tokio
SeaORM
MySQL 8
Redis
Serde
Thiserror
Tracing
Tower HTTP
JWT
RBAC

Axum 只负责 Web 层和路由体系;数据库访问、参数校验、配置管理、JWT、OpenAPI、Redis 等能力应拆成独立组件。这样比把所有功能写进 Handler 更适合你规划的多应用、多模块、Java 风格分层项目。