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 |
| セキュリティスキーム | API認証方式を記述 | #[derive(OpenApi)] + SecurityAddon |
| Swagger UI | インタラクティブAPIドキュメントUI | SwaggerUi::new("/swagger-ui") |
| Redoc | 美しいAPIドキュメント表示 | Redoc::new("/redoc") |
問題分析:5つのペインポイント
- ドキュメントとコードの乖離:YAML/JSONドキュメントを手動作成、コード変更時にドキュメント更新忘れ、API利用者が古い情報を取得
- Schema定義の重複:Rust構造体を一度定義し、OpenAPI Schemaをもう一度書く—保守コストが倍増
- 認証ドキュメントの欠落:JWT、OAuth2などの認証方式がドキュメントに反映されておらず、フロントエンドがTokenの渡し方を知らない
- マルチバージョンの混乱:v1、v2、v3のAPIドキュメントが混在し、利用者がどのバージョンを使うべきか分からない
- ドキュメントUIの簡陋さ:生のJSON/YAMLのみで、インタラクティブなUIがなく、APIテストに別ツールが必要
パターン1: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仕様を生成
パターン2:パス注釈とリクエスト/レスポンスドキュメント
#[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ステータスコードをカバー
パターン3:セキュリティスキームと認証ドキュメント
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を実装、セキュリティスキームを統一的に追加- Bearer JWT、API Key、OAuth2の3つの主要認証方式をサポート
#[utoipa::path(security(...))]で各エンドポイントの認証方式を指定- 認証不要のエンドポイント(healthなど)にはsecurity注釈を追加しない
パターン4:Swagger UIとRedoc統合
見やすいドキュメントUIは生の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で複数の環境URLを定義、切り替えテストに便利persist_authorizationでSwagger UIがTokenを記憶、毎回の再入力不要
パターン5:マルチバージョン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}")] // 単数形
async fn get_user(Path(id): Path<u64>) -> Json<User> { /* ... */ }
// ✅ 正しい:パスを完全に一致させる
#[utoipa::path(get, path = "/api/users/{id}")]
async fn get_user(Path(id): Path<u64>) -> Json<User> { /* ... */ }
落とし穴3:ジェネリックSchemaのToSchema制約欠落
// ❌ 間違い:ジェネリックレスポンスに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を実装してセキュリティスキームを追加 |
| リクエストボディが空 | 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ドキュメントは書かれるものではなく、コードから「育つ」ものです。コード即ドキュメント、ドキュメント即真実。
オンラインツールおすすめ
- JSONフォーマッター — OpenAPI JSONをフォーマット、構造を素早く確認
- cURL→コード変換 — APIリクエストを各言語のコードに変換、ドキュメントと併用
- ハッシュ計算 — API署名ハッシュを計算、インターフェースのセキュリティを確保
ブラウザローカルツールを無料で試す →