モチベーション
- 新卒フロントエンドエンジニアだけど、APIを開発したい
- 実装の詳細まではあまり触れたくない
- Rustを使いたい
参考記事
この記事を見ながら大枠を実装した。本当に参考になりました。ありがとうございます。
作ったもの
JWTで認証する機能がある掲示板API
コードの詳細はリポジトリをご覧ください。
機能
- ユーザー登録
- ユーザー認証
- 単一の掲示板に対する投稿のCRUD操作(認証されたユーザーのみ)
設計方針
- 最近DDDの勉強しているので、それっぽく作ってみる。
- データベースの構築は行わない。メモリで頑張る。
採用技術
- OPENAPI Specification
- OPENAPI Generator
- redocly
- Rust
- Axum (0.7.5)
仕組み
- OpenAPI SpecificationでAPIの仕様を記述する
- OpenAPI Generator で仕様に沿ったサーバのコードを生成する
- 生成されたコードはAPIの仕様書に沿ったもの。当たり前ではあるがロジックは含まれておらず、あくまでどのようなリクエストが来てどのようなレスポンスを返すのかのみが定義されている。よってここの段階でロジックを記述する。
- APIサーバー完成。
手順
OPENAPIの書式に沿ったyamlまたはjsonでAPIの仕様を記述する。
openapi: 3.0.3
info:
title: 掲示板アプリケーションAPI
version: 1.0.0
description: ユーザー登録、認証、および投稿のCRUD操作を備えた掲示板アプリケーションのAPI
contact:
name: Your Name
url: 'https://your-website.com'
license:
name: MIT
url: 'https://opensource.org/licenses/MIT'
servers:
- url: 'http://localhost:8080'
description: ローカルサーバー
tags:
- name: users
description: ユーザーに関する操作
- name: auth
description: ユーザー認証
- name: posts
description: 投稿に関する操作
paths:
/users:
post:
tags:
- users
summary: 新規ユーザー登録
description: 名前、メールアドレス、パスワードを使用して新規ユーザーを登録します。
requestBody:
$ref: '#/components/requestBodies/UserRequest'
responses:
'201':
$ref: '#/components/responses/UserResponse'
'400':
description: リクエストが不正です
...
OpenAPI Generatorでコードを生成する
$ openapi-generator-cli generate -i reference/spec.yaml -g rust-axum -o ./openapi_gen
ロジックは別クレートで実装する
今回はプロジェクト直下でクレート作成
Cargo.tomlの[dependecies]にopenapiを追加
[dependencies]
openapi = { path = "openapi_gen" }
ロジック部分もaxumを利用するので
dependenciesに色々追加する。
[dependencies]
axum = "0.7.5"
axum-extra = { version = "0.9.3", features = ["cookie", "multipart"] }
serde = { version = "1.0.203", features = ["derive"] }
tokio = { version = "1.38.0", features = ["full"] }
openapi = { path = "openapi_gen" }
validator = { version = "0.18.1", features = ["derive"] }
jsonwebtoken = "9.3.0"
chrono = "0.4.38"
uuid = { version = "1.9.1", features = ["v4"] }
tracing-subscriber = "0.3.18"
tracing = "0.1.40"
argon2 = { version = "0.5.3", features = ["password-hash", "rand"] }
password-hash = { version = "0.5.0", features = ["getrandom"] }
thiserror = "1.0.61"
なんちゃってDDDしかできないが、アプリケーションをmain.rsに、エンティティとバリューオブジェクト、サービスを別モジュールに切り出して実装した。
ビジネスロジックはOPENAPI Generatorで生成された各機能のトレイト(今回はUser, Posts, Authトレイト)をApiImplという構造体に実装していきます。
サーバーのmain関数は以下のようになります。
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().init();
// インメモリDB
let users = Arc::new(Mutex::new(Vec::new()));
let posts = Arc::new(Mutex::new(Vec::new()));
// ApiImpl構造体を生成
let api = ApiImpl { users, posts };
// new関数はOPENAPI Generatorが自動生成したもの
let router = new(api);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.expect("failed to bind to address");
axum::serve(listener, router).await.unwrap();
}
OPENAPI Generatorが自動生成したnew関数でaxumのrouterを作成します。
new関数の引数の構造体は、AsRefと仕様書に定義されたエンドポイントに対応したトレイト(今回はUser, Posts, Authトレイト)が全て実装されている必要があります。
APIドキュメント
redoclyを利用して、Githubのリポジトリと連携し仕様書のページを可視化する。
Redocly CLIを用いることで、ローカルで仕様書を単一のhtmlとして生成することも可能
Redocly CLI
https://redocly.com/docs/cli
感想
API仕様書からアプリケーション外とやり取りするインターフェースが生成されるので、開発者はビジネスロジックの実装に集中できるという点が一番いいなと思いました。Axum版のGeneratorはまだベータ版で自動生成されない箇所(Security Schemes等)もありますが今後に期待です。
また、RustRoverで仕様書を書いていたのですが、OPENAPIのプラグインが優秀でRedoclyとSwaggerUIどちらの見た目のプレビューも表示することができます。非常に快適です。