Rust Axum OpenAPI文檔實戰:用utoipa自動生成API文檔的5個核心模式
在API驅動的世界裡,文檔不是可選項——它是團隊協作的契約、前後端溝通的橋樑、第三方集成的入口。但手動維護OpenAPI文檔?那簡直是噩夢:改了代碼忘改文檔,文檔和實現漸行漸遠,最終變成沒人信的「裝飾品」。2026年,Rust生態的utoipa讓這一切成為歷史——用宏自動派生Schema,用注解自動生成路徑,代碼即文檔,文檔即真相。今天,老張帶你從Schema派生到路徑注解、從Swagger UI到多版本管理,徹底搞懂Rust Axum OpenAPI文檔的5個核心模式。
核心概念速覽
| 概念 | 說明 | 關鍵類型/宏 |
|---|---|---|
| Schema派生 | 自動將Rust結構體轉為OpenAPI Schema | #[derive(ToSchema)] |
| 路徑注解 | 自動將Axum路由轉為OpenAPI Path | #[derive(ToSchema)] on handlers |
| 安全Scheme | 描述API認證方式 | #[derive(OpenApi)] + SecurityAddon |
| Swagger UI | 交互式API文檔界面 | SwaggerUi::new("/swagger-ui") |
| Redoc | 精美的API文檔展示 | Redoc::new("/redoc") |
問題分析:5大痛點
- 文檔與代碼脫節:手動寫YAML/JSON文檔,改了代碼忘改文檔,API消費者拿到過時信息
- Schema定義重複:Rust結構體定義一遍,OpenAPI Schema再寫一遍,維護成本翻倍
- 認證文檔缺失:JWT、OAuth2等認證方式沒有在文檔中體現,前端不知道怎麼傳Token
- 多版本混亂:v1、v2、v3的API文檔混在一起,消費者不知道該用哪個版本
- 文檔界面簡陋:只有原始JSON/YAML,沒有可交互的UI,測試API還得另找工具
模式一:utoipa基礎配置與Schema派生
從零開始搭建utoipa + Axum項目,用宏自動派生OpenAPI Schema。
use axum::{Router, routing::get, Json};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct User {
pub id: u64,
pub username: String,
pub email: String,
#[schema(example = "active")]
pub status: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CreateUserRequest {
#[schema(example = "zhangsan")]
pub username: String,
#[schema(example = "zhangsan@toolsku.com")]
pub email: String,
#[schema(example = "secure_password_123")]
pub password: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ApiResponse<T: ToSchema> {
pub code: i32,
pub message: String,
pub data: Option<T>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ErrorResponse {
pub code: i32,
pub message: String,
}
#[derive(OpenApi)]
#[openapi(
paths(list_users, create_user, get_user),
schemas(User, CreateUserRequest, ApiResponse<User>, ErrorResponse),
tags(
(name = "用戶管理", description = "用戶CRUD操作")
)
)]
struct ApiDoc;
/// 列出所有用戶
#[utoipa::path(
get,
path = "/api/users",
responses(
(status = 200, description = "成功獲取用戶列表", body = ApiResponse<Vec<User>>)
),
tag = "用戶管理"
)]
async fn list_users() -> Json<ApiResponse<Vec<User>>> {
let users = vec![
User {
id: 1,
username: "zhangsan".into(),
email: "zhangsan@toolsku.com".into(),
status: "active".into(),
created_at: "2026-01-15".into(),
},
];
Json(ApiResponse {
code: 0,
message: "success".into(),
data: Some(users),
})
}
/// 創建新用戶
#[utoipa::path(
post,
path = "/api/users",
request_body = CreateUserRequest,
responses(
(status = 201, description = "用戶創建成功", body = ApiResponse<User>),
(status = 400, description = "請求參數錯誤", body = ErrorResponse)
),
tag = "用戶管理"
)]
async fn create_user(
Json(req): Json<CreateUserRequest>,
) -> Json<ApiResponse<User>> {
let user = User {
id: 2,
username: req.username,
email: req.email,
status: "active".into(),
created_at: "2026-06-21".into(),
};
Json(ApiResponse {
code: 0,
message: "created".into(),
data: Some(user),
})
}
/// 獲取單個用戶
#[utoipa::path(
get,
path = "/api/users/{id}",
params(
("id" = u64, Path, description = "用戶ID")
),
responses(
(status = 200, description = "成功獲取用戶", body = ApiResponse<User>),
(status = 404, description = "用戶不存在", body = ErrorResponse)
),
tag = "用戶管理"
)]
async fn get_user(
axum::extract::Path(id): axum::extract::Path<u64>,
) -> Json<ApiResponse<User>> {
Json(ApiResponse {
code: 0,
message: "success".into(),
data: Some(User {
id,
username: "zhangsan".into(),
email: "zhangsan@toolsku.com".into(),
status: "active".into(),
created_at: "2026-01-15".into(),
}),
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.merge(SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/api/users", get(list_users).post(create_user))
.route("/api/users/{id}", get(get_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("伺服器啟動: http://0.0.0.0:3000");
println!("Swagger UI: http://0.0.0.0:3000/swagger-ui/");
axum::serve(listener, app).await.unwrap();
}
關鍵要點:
#[derive(ToSchema)]自動將結構體轉為OpenAPI Schema,無需手寫YAML#[schema(example = "...")]為字段添加範例值,文檔更友好#[derive(OpenApi)]匯總所有路徑和Schema,生成完整的OpenAPI規範
模式二:路徑注解與請求/響應文檔
深入#[utoipa::path]注解,精確控制每個API端點的文檔細節。
use axum::{Json, extract::{Path, Query}};
use serde::Deserialize;
use utoipa::{ToSchema, IntoParams};
#[derive(Debug, Deserialize, IntoParams)]
pub struct ListUsersQuery {
/// 頁碼,從1開始
#[param(example = 1)]
pub page: Option<u32>,
/// 每頁數量
#[param(example = 20)]
pub per_page: Option<u32>,
/// 按用戶名搜索
pub search: Option<String>,
/// 用戶狀態過濾
pub status: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum UserRole {
Admin,
Editor,
Viewer,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserDetail {
#[schema(ref = "User")]
pub basic: User,
pub role: UserRole,
pub last_login: Option<String>,
pub login_count: u32,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct PaginatedResponse<T: ToSchema> {
pub code: i32,
pub message: String,
pub data: Vec<T>,
pub total: u64,
pub page: u32,
pub per_page: u32,
}
/// 分頁查詢用戶列表
#[utoipa::path(
get,
path = "/api/v1/users",
params(
ListUsersQuery
),
responses(
(status = 200, description = "成功獲取用戶列表", body = PaginatedResponse<UserDetail>),
(status = 400, description = "請求參數錯誤", body = ErrorResponse),
(status = 500, description = "伺服器內部錯誤", body = ErrorResponse)
),
security(
("bearer_auth" = [])
),
tag = "用戶管理"
)]
async fn list_users_v1(
Query(query): Query<ListUsersQuery>,
) -> Json<PaginatedResponse<UserDetail>> {
let page = query.page.unwrap_or(1);
let per_page = query.per_page.unwrap_or(20);
Json(PaginatedResponse {
code: 0,
message: "success".into(),
data: vec![],
total: 100,
page,
per_page,
})
}
/// 更新用戶信息
#[utoipa::path(
put,
path = "/api/v1/users/{id}",
params(
("id" = u64, Path, description = "用戶ID")
),
request_body(
content = CreateUserRequest,
description = "用戶更新信息",
content_type = "application/json"
),
responses(
(status = 200, description = "更新成功", body = ApiResponse<UserDetail>),
(status = 404, description = "用戶不存在", body = ErrorResponse),
(status = 422, description = "數據驗證失敗", body = ErrorResponse)
),
security(
("bearer_auth" = [])
),
tag = "用戶管理"
)]
async fn update_user(
Path(id): Path<u64>,
Json(req): Json<CreateUserRequest>,
) -> Json<ApiResponse<UserDetail>> {
Json(ApiResponse {
code: 0,
message: "updated".into(),
data: None,
})
}
/// 刪除用戶
#[utoipa::path(
delete,
path = "/api/v1/users/{id}",
params(
("id" = u64, Path, description = "用戶ID")
),
responses(
(status = 204, description = "刪除成功"),
(status = 404, description = "用戶不存在", body = ErrorResponse)
),
security(
("bearer_auth" = [])
),
tag = "用戶管理"
)]
async fn delete_user(
Path(id): Path<u64>,
) -> Json<ApiResponse<()>> {
Json(ApiResponse {
code: 0,
message: "deleted".into(),
data: None,
})
}
關鍵要點:
IntoParams自動將查詢參數結構體轉為OpenAPI參數#[param(example = ...)]為參數添加範例request_body(content = ..., content_type = ...)精確描述請求體- 多個
responses覆蓋所有可能的HTTP狀態碼
模式三:安全Scheme與認證文檔
API文檔必須描述認證方式,否則前端不知道怎麼傳Token。
use utoipa::openapi::security::{SecurityAddon, HttpAuthScheme, HttpBuilder, SecurityScheme};
use utoipa::OpenApi;
use axum::{Router, http::HeaderMap};
#[derive(OpenApi)]
#[openapi(
paths(list_users_v1, update_user, delete_user, health_check),
schemas(User, UserDetail, CreateUserRequest, ApiResponse, ErrorResponse),
modifiers(&SecurityAddon),
tags(
(name = "用戶管理", description = "用戶CRUD操作"),
(name = "系統", description = "系統健康檢查")
)
)]
struct ApiDoc;
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"bearer_auth",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.description("請輸入JWT Token,格式:Bearer <token>")
.build(),
),
);
components.add_security_scheme(
"api_key",
SecurityScheme::ApiKey(
utoipa::openapi::security::ApiKey::Header(
utoipa::openapi::security::ApiKeyBuilder::new()
.name("X-API-Key")
.description("請在Header中傳入API Key")
.build(),
),
),
);
components.add_security_scheme(
"oauth2",
SecurityScheme::OAuth2(
utoipa::openapi::security::OAuth2Builder::new()
.flow(
utoipa::openapi::security::ImplicitBuilder::new()
.authorization_url("https://auth.toolsku.com/authorize")
.scope("read:users", "讀取用戶信息")
.scope("write:users", "修改用戶信息")
.build(),
)
.build(),
),
);
}
}
}
/// 健康檢查(無需認證)
#[utoipa::path(
get,
path = "/api/health",
responses(
(status = 200, description = "服務正常")
),
tag = "系統"
)]
async fn health_check() -> &'static str {
"OK"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.merge(utoipa_swagger_ui::SwaggerUi::new("/swagger-ui/{_:.*}")
.url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/api/health", axum::routing::get(health_check));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Swagger UI: http://0.0.0.0:3000/swagger-ui/");
axum::serve(listener, app).await.unwrap();
}
關鍵要點:
SecurityAddon實現Modifytrait,統一添加安全Scheme- 支持Bearer JWT、API Key、OAuth2三種主流認證方式
#[utoipa::path(security(...))]指定每個端點需要的認證方式- 無需認證的端點(如health)不添加security注解
模式四:Swagger UI與Redoc集成
一個好看的文檔界面比原始JSON有用一百倍。
use axum::Router;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use utoipa_redoc::{Redoc, Servable};
use utoipa_rapidoc::RapiDoc;
#[derive(OpenApi)]
#[openapi(
paths(list_users, create_user, get_user, health_check),
schemas(User, CreateUserRequest, ApiResponse, ErrorResponse),
modifiers(&SecurityAddon),
tags(
(name = "用戶管理", description = "用戶CRUD操作"),
(name = "系統", description = "系統健康檢查")
),
info(
title = "ToolsKu API",
version = "1.0.0",
description = "ToolsKu在線工具平台API文檔\n\n## 快速開始\n1. 獲取JWT Token\n2. 在Swagger UI中點擊Authorize\n3. 輸入Bearer Token\n4. 開始調用API",
contact(
name = "ToolsKu Team",
email = "api@toolsku.com",
url = "https://toolsku.com"
)
),
servers(
(url = "https://api.toolsku.com", description = "生產環境"),
(url = "https://staging-api.toolsku.com", description = "預發布環境"),
(url = "http://localhost:3000", description = "本地開發")
)
)]
struct ApiDoc;
#[tokio::main]
async fn main() {
let openapi = ApiDoc::openapi();
let app = Router::new()
.merge(SwaggerUi::new("/swagger-ui/{_:.*}")
.url("/api-docs/openapi.json", openapi.clone())
.config(
utoipa_swagger_ui::Config::default()
.try_it_out_enabled(true)
.filter(true)
.persist_authorization(true)
.display_request_duration(true)
)
)
.merge(Redoc::with_url("/redoc", openapi.clone())
.with_config(utoipa_redoc::Config::with_custom_html(r#"
<style>
.api-content { max-width: 1200px; }
</style>
"#))
)
.merge(RapiDoc::with_url("/rapidoc", openapi))
.route("/api/users", axum::routing::get(list_users).post(create_user))
.route("/api/users/{id}", axum::routing::get(get_user))
.route("/api/health", axum::routing::get(health_check));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("🚀 伺服器啟動: http://0.0.0.0:3000");
println!("📖 Swagger UI: http://0.0.0.0:3000/swagger-ui/");
println!("📕 Redoc: http://0.0.0.0:3000/redoc/");
println!("📋 RapiDoc: http://0.0.0.0:3000/rapidoc/");
axum::serve(listener, app).await.unwrap();
}
關鍵要點:
- Swagger UI適合開發調試,支持「Try it out」直接測試API
- Redoc適合對外展示,排版精美,適合給非技術人員看
servers定義多個環境地址,方便切換測試persist_authorization讓Swagger UI記住Token,不用每次重新輸入
模式五:多版本API文檔管理
v1、v2、v3並存是常態,每個版本需要獨立的文檔。
use axum::Router;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserV1 {
pub id: u64,
pub name: String,
pub email: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CreateUserV1Request {
pub name: String,
pub email: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserV2 {
pub id: String,
pub username: String,
pub email: String,
pub avatar_url: Option<String>,
pub role: String,
}
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CreateUserV2Request {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(OpenApi)]
#[openapi(
paths(v1_list_users, v1_create_user),
schemas(UserV1, CreateUserV1Request),
info(
title = "ToolsKu API V1",
version = "1.0.0",
description = "**已廢棄** - 請遷移到V2 API\n\nV1 API將在2026年12月31日停止服務。"
),
tags(
(name = "用戶V1", description = "V1用戶接口(已廢棄)")
)
)]
struct ApiDocV1;
#[derive(OpenApi)]
#[openapi(
paths(v2_list_users, v2_create_user),
schemas(UserV2, CreateUserV2Request),
info(
title = "ToolsKu API V2",
version = "2.0.0",
description = "ToolsKu V2 API - 全面升級\n\n## V1 → V2 遷移指南\n- `id` 從數字改為UUID字符串\n- `name` 改為 `username`\n- 新增 `avatar_url` 和 `role` 字段"
),
modifiers(&SecurityAddon),
tags(
(name = "用戶V2", description = "V2用戶接口(推薦)")
)
)]
struct ApiDocV2;
#[utoipa::path(
get,
path = "/api/v1/users",
responses(
(status = 200, description = "獲取用戶列表V1", body = Vec<UserV1>)
),
tag = "用戶V1",
deprecated
)]
async fn v1_list_users() -> Json<Vec<UserV1>> {
Json(vec![])
}
#[utoipa::path(
post,
path = "/api/v1/users",
request_body = CreateUserV1Request,
responses(
(status = 201, description = "創建用戶V1", body = UserV1)
),
tag = "用戶V1",
deprecated
)]
async fn v1_create_user(Json(req): Json<CreateUserV1Request>) -> Json<UserV1> {
Json(UserV1 { id: 1, name: req.name, email: req.email })
}
#[utoipa::path(
get,
path = "/api/v2/users",
responses(
(status = 200, description = "獲取用戶列表V2", body = Vec<UserV2>)
),
security(
("bearer_auth" = [])
),
tag = "用戶V2"
)]
async fn v2_list_users() -> Json<Vec<UserV2>> {
Json(vec![])
}
#[utoipa::path(
post,
path = "/api/v2/users",
request_body = CreateUserV2Request,
responses(
(status = 201, description = "創建用戶V2", body = UserV2)
),
security(
("bearer_auth" = [])
),
tag = "用戶V2"
)]
async fn v2_create_user(Json(req): Json<CreateUserV2Request>) -> Json<UserV2> {
Json(UserV2 {
id: "550e8400-e29b-41d4-a716-446655440000".into(),
username: req.username,
email: req.email,
avatar_url: None,
role: "viewer".into(),
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.merge(SwaggerUi::new("/swagger-ui/v1/{_:.*}")
.url("/api-docs/v1/openapi.json", ApiDocV1::openapi()))
.merge(SwaggerUi::new("/swagger-ui/v2/{_:.*}")
.url("/api-docs/v2/openapi.json", ApiDocV2::openapi()))
.route("/api/v1/users", axum::routing::get(v1_list_users).post(v1_create_user))
.route("/api/v2/users", axum::routing::get(v2_list_users).post(v2_create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("🚀 伺服器啟動: http://0.0.0.0:3000");
println!("📖 V1文檔: http://0.0.0.0:3000/swagger-ui/v1/");
println!("📖 V2文檔: http://0.0.0.0:3000/swagger-ui/v2/");
axum::serve(listener, app).await.unwrap();
}
關鍵要點:
- 每個API版本獨立的
#[derive(OpenApi)],互不干擾 - V1接口標記
deprecated,Swagger UI會顯示刪除線 - V2文檔中包含遷移指南,幫助用戶平滑過渡
- 不同版本的Swagger UI路徑分開,避免混淆
踩坑指南
坑1:忘記在OpenApi中註冊Schema
// ❌ 錯誤:Schema定義了但沒註冊
#[derive(ToSchema)]
struct User { /* ... */ }
#[derive(OpenApi)]
#[openapi(paths(list_users))] // 缺少schemas!
struct ApiDoc;
// ✅ 正確:在openapi宏中註冊所有Schema
#[derive(OpenApi)]
#[openapi(
paths(list_users),
schemas(User, CreateUserRequest)
)]
struct ApiDoc;
坑2:路徑注解與Axum路由不匹配
// ❌ 錯誤:注解路徑和Axum路由不一致
#[utoipa::path(get, path = "/api/user/{id}")] // user單數
async fn get_user(Path(id): Path<u64>) -> Json<User> { /* ... */ }
let app = Router::new()
.route("/api/users/{id}", get(get_user)); // users複數
// ✅ 正確:保持路徑完全一致
#[utoipa::path(get, path = "/api/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Json<User> { /* ... */ }
坑3:泛型Schema未正確標註
// ❌ 錯誤:泛型響應沒有約束ToSchema
#[derive(Serialize, ToSchema)]
pub struct ApiResponse<T> {
pub data: Option<T>,
}
// ✅ 正確:泛型參數需要ToSchema約束
#[derive(Serialize, ToSchema)]
pub struct ApiResponse<T: ToSchema> {
pub data: Option<T>,
}
坑4:Swagger UI路徑配置錯誤
// ❌ 錯誤:Swagger UI路徑和API路由衝突
.merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/swagger", get(some_handler));
// ✅ 正確:使用不同的路徑前綴
.merge(SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/api/users", get(list_users));
坑5:枚舉Schema缺少serde rename
// ❌ 錯誤:枚舉值在JSON中顯示為PascalCase
#[derive(Serialize, Deserialize, ToSchema)]
pub enum Status {
Active,
Inactive,
}
// ✅ 正確:添加serde rename_all
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Active,
Inactive,
}
錯誤排查表
| 錯誤現象 | 可能原因 | 排查方法 | 解決方案 |
|---|---|---|---|
| Swagger UI空白 | OpenApi未正確配置 | 檢查/api-docs/openapi.json是否返回有效JSON |
確保paths和schemas都已註冊 |
| Schema顯示為空 | 忘記註冊Schema | 檢查#[openapi(schemas(...))] |
在openapi宏中添加所有Schema |
| Try it out 404 | 路徑注解與路由不匹配 | 對比注解path和Router route | 確保路徑完全一致 |
| 認證按鈕不顯示 | 未添加SecurityAddon | 檢查modifiers配置 |
實現Modify trait添加安全Scheme |
| 請求體顯示為空 | 未指定request_body | 檢查#[utoipa::path]注解 |
添加request_body = XxxRequest |
| 枚舉值格式錯誤 | 缺少serde rename | 檢查枚舉的JSON輸出 | 添加#[serde(rename_all = "...")] |
| 泛型Schema報錯 | 泛型參數缺少ToSchema約束 | 檢查編譯錯誤 | 添加T: ToSchema約束 |
| 多版本文檔串 | OpenApi定義共享了paths | 檢查各版本的#[openapi] |
每個版本獨立的OpenApi struct |
| Redoc樣式異常 | 自定義CSS衝突 | 檢查with_config中的HTML | 簡化自定義樣式 |
| 中文亂碼 | 響應頭缺少charset | 檢查Content-Type頭 | 確保返回application/json; charset=utf-8 |
進階優化
-
CI/CD文檔校驗:在CI中運行
cargo test自動校驗OpenAPI規範是否完整,防止文檔與代碼脫節 -
文檔自動發布:構建時生成openapi.json,自動發布到文檔站點,團隊成員隨時可查
-
請求/響應範例:使用
#[schema(example = json!(...))]添加完整的請求/響應範例,讓文檔更實用 -
自定義錯誤類型:為每種HTTP錯誤定義專門的Schema(如ValidationError、AuthError),文檔更精確
-
Axum宏集成:使用
utoipa-axum的OpenApiRouter自動從Axum路由推導OpenAPI路徑,減少手動注解
方案對比
| 方案 | 自動化程度 | 維護成本 | 適用場景 |
|---|---|---|---|
| 手寫OpenAPI YAML | ⭐ | ⭐⭐⭐⭐⭐ | 極簡API |
| utoipa Schema派生 | ⭐⭐⭐⭐ | ⭐⭐ | 大多數項目 |
| utoipa + SecurityAddon | ⭐⭐⭐⭐ | ⭐⭐ | 需要認證的API |
| utoipa-axum自動推導 | ⭐⭐⭐⭐⭐ | ⭐ | 新項目首選 |
| 多版本獨立管理 | ⭐⭐⭐ | ⭐⭐⭐ | 多版本並存 |
總結
Rust Axum + utoipa讓API文檔從「手工坊」進化到「自動化流水線」——Schema派生消除了重複定義,路徑注解確保文檔與代碼同步,SecurityAddon讓認證不再是個謎,Swagger UI/Redoc讓文檔可交互可展示。 記住:好的API文檔不是寫出來的,而是從代碼中「長」出來的。代碼即文檔,文檔即真相。
在線工具推薦
本站提供瀏覽器本地工具,免註冊即可試用 →