Python の FastAPI のように、Rust でもWebサーバー側で OpenAPI を生成させたいと考えていました。
axum の Issue を覗いてみると、 aide というクレートが有望そうです。
(axum
自身は現状ロードマップに取り込むつもりがないようです)
下記が調査で得られた成果物です。Rust の型変更をするだけで、フロントのAPIクライアントがリアルタイムで定義変更を検知します。
(画像の解像度が悪いですが、左が TypeScript, 右が Rust のコードです)
- Rust 側のスキーマ変更を cargo watch で監視
- aide で OpenAPI を自動生成
- OpenAPI から TypeScript の型を自動生成
- フロントの axios クライアントが API の定義変更を自動検知
実際に触ってみた感想をまとめました。
ドキュメントは少ない
crate の README.md は非常に少ないです。 examples を参考に調査する必要があります。
どのように実装されているのか?
axum のルータを OpenAPI も扱える様に自前実装しています。
次の様な使い方です。
let mut api = aide::openapi::OpenApi::default();
let app = aide::axum::ApiRouter::new()
.api_route(
"/todo/:id",
aide::axum::routing::get(get_todo)
.delete(delete_todo),
)
.api_route(
"/todo/:id/complete",
aide::axum::routing::put(complete_todo)
)
.nest_api_service(
"/docs",
docs_routes()
)
.finish_api(&mut api)
.layer(Extension(Arc::new(api)));
基本的には axum::routing::*
を aide::axum::routing::*
で置き換えれば良い様です。
OpenAPI も出力できる様になる理由を探るため、 axum::routing::get
のトレイトを比較してみましょう。
// axum の場合
pub fn get<H, T>(self, handler: H) -> Self
where
H: Handler<T, S, B>,
T: 'static,
S: Send + Sync + 'static,
// aide の場合
pub fn get<H, I, O, T>(self, handler: H) -> Self
where
H: Handler<T, S, B> + OperationHandler<I, O>,
I: OperationInput,
O: OperationOutput,
B: Send + 'static,
T: 'static,
aide
はトレイトが増えていますね。 OperationHandler
というものが増え、このトレイトは axum
本家の Router では知ることのできない OperationInput
と OperationOutput
の情報を追加で持っています。
つまり、 aide
の大まかなコンセプトは下記の様になっています。
- 通常の API サーバとして動作させるとき:
Handler<T, S, B>
を用いる(axum
と同様) - OpenAPI を生成したいとき:
OperationHandler<I, O>
を用いる
なるほど!
aide と FastAPI との比較
両方を利用してみたときの感想です。
aide のメリット
- エラーレスポンスも型から自動生成できる様になる(例外ではなく Result でエラーを扱うため)
aide のデメリット
- API のスキーマだけを変更した場合、handler 内のロジックが壊れるので、
todo!
を仕込む必要がある - トレイトが複雑なのでデバックに時間がかかる
- ビルドが遅い
Fast API のメリット
- API のスキーマだけを変更するだけで OpenAPI が出力可能(自動テストは壊れるが、後で直せば良い)
- ドキュメントが豊富
Fast API のデメリット
- エラーレスポンスを自動で生成できない
- OpenAPI の出力結果は間違っている部分がある(Option の扱いとか。 Pydantic V2 に期待)
感想
動かせる様になるまで少し時間がかかりましたが、上手く機能しています。
今後の成長に期待です!