目录
Axum 详细使用教程
本文基于 Axum 0.8.9、tower-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 参数从左到右执行。请求体是只能消费一次的异步数据流,因此 Json、String、Bytes、Request 等会消费请求体的 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 的 Service 和 Layer 体系。普通项目可使用 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 风格分层项目。