响应
标准化 API 响应:ApiResponse<T>
为了确保我们的 API 接口具有一致性、可预测性,并让前后端开发者都能轻松协作,我们实现了一套标准化的 API 响应结构,即 ApiResponse<T>
。
这个模块处理了所有创建 HTTP 响应、设置状态码和格式化 JSON 的模板代码,使得我们的业务逻辑(在 Handler 和 Service 中)能够更加简洁,并专注于核心功能。
黄金法则
本项目中 所有 的 API 端点都应该返回由 ApiResponse
生成的响应。Handler 中不应再出现手动创建 Json
或 (StatusCode, ...)
元组的情况。
JSON 响应结构
我们 API 的每一个响应都将遵循以下 JSON 结构。这种一致性对于 API 的消费者(如前端应用)至关重要。
成功时
当一个操作成功时,data
字段将包含请求的业务数据。
{
"message": "操作成功",
"data": {
"id": 123,
"username": "alice",
"email": "alice@example.com"
}
}
失败或无数据时
当发生错误,或者某个操作成功但无需返回业务数据时(例如 DELETE
请求),data
字段将是一个空对象 {}
。客户端可以依赖 message
字段和 HTTP 状态码来获取上下文信息。
{
"message": "未找到指定的用户",
"data": {}
}
核心结构体定义
整个响应系统围绕两个核心结构体构建:
// 泛型的主响应结构体
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ApiResponse<T> {
pub message: String,
pub data: T,
}
// 当没有业务数据需要返回时的占位类型
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EmptyData {}
泛型参数 T
允许我们返回任何可以被序列化为 JSON 的数据类型(即实现了 serde::Serialize
Trait 的类型)。对于错误响应或无数据响应,我们统一使用 ApiResponse<EmptyData>
。
如何使用 ApiResponse
该模块提供了一套丰富的辅助函数,覆盖了所有常见的 API 响应场景。你应该 始终 使用这些辅助函数,而不是手动构造 ApiResponse
实例。
1. 成功响应 (200 OK
, 201 Created
)
这些函数用于当你的操作成功并需要返回数据时。
✅ 基本成功响应 (200 OK
)
这是最常见的成功响应。
async fn get_user_profile(user_info: UserInfo) -> Response {
// ... 获取用户的逻辑 ...
let user_data = UserData { id: user_info.uid, ... };
// 返回一个 200 OK 响应,包含默认消息和 user_data
ApiResponse::ok(user_data)
}
✅ 带自定义消息的成功响应 (200 OK
)
async fn update_user_profile(
user_info: UserInfo,
VJson(payload): VJson<UpdateProfilePayload>,
) -> Response {
// ... 更新个人资料的逻辑 ...
ApiResponse::ok_with_msg(EmptyData {}, "个人资料更新成功!")
}
✅ 资源创建成功 (201 Created
)
当一个新资源被成功创建时(例如,在一个 POST
请求后),使用此函数。
async fn register_user(VJson(payload): VJson<RegisterPayload>) -> Response {
let new_user = user_service::create_user(payload).await?;
// 返回一个 201 Created 响应,包含默认消息
ApiResponse::created(new_user)
}
2. 错误响应 (4xx
, 5xx
)
该模块为所有标准的 HTTP 错误码提供了快捷方式。这些函数会自动设置正确的状态码,并将你的错误消息包装在标准的响应结构中。
async fn get_article(Path(id): Path<i64>) -> Response {
match article_service::find_by_id(id).await {
Ok(Some(article)) => ApiResponse::ok(article),
Ok(None) => ApiResponse::not_found("找不到指定的文章"), // 返回 404 Not Found
Err(_) => ApiResponse::internal_server_error("数据库查询失败"), // 返回 500 Internal Server Error
}
}
async fn some_admin_action(user: UserInfo) -> Response {
if !user.is_admin() {
// 返回 403 Forbidden
return ApiResponse::forbidden("您没有权限执行此操作");
}
// ... 继续执行业务逻辑 ...
}
3. 最强大的辅助函数:from_result
这是处理来自 Service 层返回结果的 首选方式。我们的 Service 函数通常返回一个 Result<T, Error>
。ApiResponse::from_result
函数可以自动将其转换为正确的 HTTP 响应。
- 如果是
Ok(data)
,它会返回一个包含数据的200 OK
响应。 - 如果是
Err(error)
,它会检查错误类型,并返回相应的4xx
或5xx
响应。
// 在你的 Service 层
pub async fn get_user_details(id: i64) -> Result<User> {
let user = User::find_by_id(id).one(DB).await?
.ok_or_else(|| Error::Message("User not found".to_string()))?;
Ok(user)
}
// 在你的 Handler 中 - 注意代码是多么简洁!
async fn handler_get_user(Path(id): Path<i64>) -> Response {
let result = user_service::get_user_details(id).await;
ApiResponse::from_result(result)
}
这种模式完美地将业务逻辑与响应格式化分离开来。Handler 的唯一职责就是调用 Service,然后将结果传递给 ApiResponse
。
4. 宏快捷方式 (可选,但很方便)
为了代码更加简洁,你也可以使用我们提供的宏。它们是围绕辅助函数的简单包装。
async fn example() -> Response {
// 成功
ApiResponse::ok_with_msg(data, "成功!");
// 失败
ApiResponse::bad_request("无效的输入");
}
async fn example() -> Response {
// 成功
api_ok!(data, "成功!");
// 失败
api_error!(400, "无效的输入");
}
实现原理:impl IntoResponse
让我们的 Handler 能够直接返回 ApiResponse<T>
的“魔法”在于它实现了 Axum 的 IntoResponse
Trait。
impl<T> IntoResponse for ApiResponse<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
// 1. 将 ApiResponse 结构体序列化为 JSON 字符串。
// 2. 如果序列化失败,返回一个 500 内部服务器错误。
// 3. 设置正确的 HTTP 头,如 Content-Type。
// 4. 将 HTTP 头和 JSON 体组合成最终的 Axum Response 对象。
// (注意:实际的 HTTP 状态码是在辅助函数中与响应体一起传递给 Axum 的)
}
}
这个实现完全隐藏了构建 HTTP 响应的复杂性,为我们的应用层代码提供了一个干净且类型安全的接口。