OpenAPI定義からTypeScript型を生成し、フロントエンド・バックエンド間でスキーマ駆動開発
最近、案件でGraphQLを使ったスキーマ駆動開発を行ったところ体験が非常に良かったため、OpenAPIでもスキーマ駆動開発を試してみました。
普及度でいうとOpenAPI Generatorの方が高そうですが、今回はAspidaエコシステムに乗ってみます。
Aspidaファミリーのopenapi2aspidaでOpenAPI YAMLファイルからTypeScriptの型定義を生成し、フロントエンド・バックエンドでimportして利用するようにします。また、バックエンドフレームワークにExpressを使用しているという前提で、express-openapi-validatorを設定しOpenAPIスキーマを元によしなにバリデーションするようにします。
Aspida選定の理由
OpenAPI GeneratorがJava製なのに対し、AspidaはTypeScript製のため、フロントエンドとバックエンドをTypeScriptで開発している場合はnpm経由で比較的ハマらず導入しやすい点、また、個人的にAspidaの方が生成される型が好みだった点があります。
フロントエンドとバックエンドの言語がTypeScriptでない場合は、クロスプログラミング言語対応のOpenAPI Generatorを選定する、もしくはツールを併用するなど検討する必要がありそうです。
環境
- node 16.17.0
- npm 8.19.2
- typescript 4.5.4
- aspida 1.11.0
- openapi2aspida 0.19.0
- express 4.18.1
- express-openapi-validator 4.13.8
- husky 7.0.4
- lint-staged 12.1.7
サンプルOpenAPIファイル
まず、以下のようなOpenAPIファイルを用意しました。
openapi: 3.0.0
info:
title: SampleOpenApi
version: 0.0.0
servers:
- url: http://localhost:3000
description: Local
- url: http://api.example.com
description: Production
paths:
/users:
post:
operationId: users.post
description: Create user.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserParams'
responses:
'200':
description: OK.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserResult'
components:
schemas:
CreateUserParams:
description: Create user params.
type: object
properties:
name:
type: string
example: 田中太郎
required:
- name
CreateUserResult:
description: Create user result.
type: object
properties:
id:
type: string
example: U1234
name:
type: string
example: 田中太郎
required:
- id
- name
openapi2aspidaでTS型定義ファイルを生成
使い方
以下の生成コマンドを叩きます。
openapi2aspida -i ./openapi.yml -o src/generated/api
上記コマンドの場合、 src/generated/api/@types/index.ts
に以下のような型定義が生成されます。
/* eslint-disable */
/** Create user params. */
export type CreateUserParams = {
name: string
}
/** Create user result. */
export type CreateUserResult = {
id: string
name: string
}
生成された型定義をフロントエンドとバックエンドでimportして利用
生成された型定義をアプリケーションコードでimportして利用します。
import { CreateUserParams, CreateUserResult } from './generated/api/@types'
Aspidaが生成する型は基本的に「APIクライアント」としての用途を意図していると思いますが、バックエンドAPI側におけるリクエストとレスポンスの型付けとしても使用可能であり静的解析の恩恵をプラスできると考えています(この点、もしより良い方法をご存知の方がいらっしゃればコメント頂ければと思います)。
上書きの際は先に既存ファイルを削除する
上書きでコード生成しようとすると下記のエラーが発生することがあります。
Error: ENOENT: no such file or directory, scandir 'api'
こちらを回避するために、先に rm -rf src/generated/api
を実行し既存ファイルを削除するようにします。npm scriptsにまとめるなら次のようになるでしょう。
{
"scripts": {
"codegen": "rm -rf src/generated/api && openapi2aspida -i ../../openapi.yml -o src/generated/api"
}
}
上記のTwitterスレッドで作者のSolufaさんが肯定しているため、こちらの対処で合っていると思われます。
git管理はしない方針
自動生成されたTSコードをgit管理に含めるべきかは悩ましいところです。
以下はOpenAPIでなくGraphQLツールのDiscussionですが参考にしてみます。
上記の議論を参考に、git管理しない方針に寄せてみます。 .gitignore
に generated
ディレクトリを加えます。
+generated
git管理しない場合、 openapi
に変更があった際にコード再生成を忘れてしまう懸念があるので、 package.json
の scripts.prepare
で npm install
の度に実行する、husky+lint-stagedを使ってファイルに変更がある度に実行するなどの設定をすると良さそうです。
{
"scripts": {
"prepare": "husky install && npm run codegen"
}
}
{
"lint-staged": {
"openapi.yml": [
"bash -c 'npm run codegen'"
]
}
}
express-openapi-validatorでAPIバリデーション
バックエンドフレームワークにExpressを使っている場合、express-openapi-validatorを組み合わせることでバリデーションをある程度自動で設定することができます。
Expressのミドルウェアに加えることで簡単に導入できます。
import 'express-openapi-validator'
app.use(
openApiValidator.middleware({
apiSpec: './openapi.yml',
validateRequests: true,
validateResponses: true,
}),
);
In my opinion
なぜスキーマファーストか
本記事は全体的に、スキーマファーストアプローチを採り、OpenAPIスキーマファイルを正とすることを強めに意識したものになります。
コードファーストとしてフレームワークやライブラリからOpenAPIファイルを生成する利点も大きいと思うのですが、一言語の一実装よりも複数言語間で標準化された仕様であるOpenAPI仕様の方が技術としての寿命が長いと考えるため、どちらかといえば開発者がスキーマファイルをメンテする形の方が好ましいと考えています。
また、コードファーストアプローチは基本的にバックエンドコードからスキーマファイルを生成するため、フロントエンドエンジニアがバックエンドの実装を待つケースが発生するように感じました。この点、スキーマファーストの場合は最初にスキーマファイルさえ策定できれば、フロントエンドエンジニアはMSWなどでモックを定義し即開発を始めるといった動きがしやすいと思っています。
最も、今の所自分はコードファースト開発の経験がないので、経験してみたら考えが変わるかもしれません。
型定義のみ利用し、ツールと一定の距離感を保つ選択肢
ここは特に個人的な考えが強いところになりますが、生成されたAPIクライアント実装は利用せず、型定義だけ利用するようにすると利便性・ツールとの距離感においてバランスが良くなるのではないかと思っています。
長期的に見ると、
- ツールのトレンド状況が大きく変化した
- 現在採用しているツールでは実現が難しい要求に突き当たった
などの理由によりツールを乗り換えたくなる可能性はあると考えるためです。
TypeScript の型生成における OpenAPI Generator のハマりどころ - READYFOR Tech Blog
READYFOR ではフロント側のジェネレーターとして typescript-fetch を選択しています。 また、生成されたコードのうち API クライアントは使用せずに型定義のみを使用しています。
こちらのブログによると、(AspidaではなくOpenAPI Generatorなのと、同じ理由かは不明ですが)READYFORさんも近い選択をしているようです。
一方、APIクライアントを使わないことでせっかくのAspidaのメリットをかなり失ってしまうことも事実であり、開発速度を重視しツールに強く依存する選択をすることももちろん有力です。その場合も、いざという時の乗り換えコストを見積もっておいたり、いわゆる「式年遷宮」的に大幅にコードを書き換える工数が発生する可能性があることをステークホルダーと握っておくなどの備えはしておくに越したことはないでしょう。
参考
- ぽふ@あとべさんはTwitterを使っています: 「ほんとにaspida、pathpidaは良いライブラリ。 aspidaでopenapi2aspidaの実行をしたときに既に出力してあるものの上書きが出来ないから、実行コマンドにrm -rf 混ぜてディレクトリ消してから実行してるけどみんなそんな感じなのかな」 / Twitter
- aspida (openapi2aspida) で任意のディレクトリに生成した結果を出力する方法
- VSCodeでOpenAPI Specificationドキュメントを編集する際に便利なプラグインたち | DevelopersIO
- 月間2万DL突破!REST APIを型安全にする最強のTypeScript製HTTPクライアントaspidaを始めよう
- OpenAPITools/openapi-generator: OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
- aspida (openapi2aspida) で任意のディレクトリに生成した結果を出力する方法
- TypeScript の型生成における OpenAPI Generator のハマりどころ - READYFOR Tech Blog
- OpenAPI、コードから書くかスキーマから書くか | Raccoon Tech Blog [株式会社ラクーンホールディングス 技術戦略部ブログ]
- Nest.js+GraphQLにおける開発手法とORMに関する考察|fumi|note
- NestJSでのGraphQLアプリ開発手法 - Fusic Tech Blog
Discussion