OpenAPI(Swagger)を利用してTypeScriptのAPI Clientを自動生成する設計と実装
追記 (2021/11/01)
手前味噌ではありますが、自作したTypeScript Code Generatorの記事も参照してみてください。 OpenAPIのenumがUnionTypeで吐き出されないなど、細かい問題を解決しています。
OpenAPI Specification(OAS)とはなにか
- OpenAPI SpecificationはAPIを記述するため規格で、旧名がSwaggerとなっている。
- HTTP APIをインターフェースとするSchema定義になっているため、相手が
WEB
でなくても良い。- → すなわち、WEB APIではなくてシステム間のプロトコルベースがHTTPであればOpenAPI Schemaで記述可能となる。
- Schemaの例) Develop with Docker Engine API
- 生成例) Himenon/docker-typescript-openapi
百聞一見に如かず
簡単な例として、/api/article
という記事を作成するAPIに対して、POST Methodで通信するとき、Request/ResponseのInterfaceを次のように定義する。
interface RequestBody {
title: string
body: string
}
interface ResponseBody {
id: string;
}
これをOASで書き起こすと次のようになる。
paths:
/api/article:
post:
operationId: createArticle # コード自動生成時の関数名となる
summary: 記事を作成するAPI
requestBody:
content:
application/json:
schema:
type: object
additionalProperties: false # Objectの拡張を許さない
required: # requiredがないObjectのValueはOptional[?]扱いになる
- title
- body
properties:
title:
type: string
body:
type: string
responses:
200: # HTTP Status Code
description: 記事が正常に作成された
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- id
properties:
id:
type: number
description: 記事のID
schema
の部分がデータの定義で、それ以外はHTTP通信や説明などに関する情報を記述する。
何度も書くとドキュメントを見なくても勝手にかけてしまうので恐れることはない。
なぜOpenAPI Specificationで書くか
- OAS3.0で記述すことにより、API仕様書はどの言語にも依存していない
- API仕様書からさまざまな言語に出力するライブラリが存在する
- REST APIは仕様書があれば実装を生成できる世界なので人間が介在する必要のない分野である
- 品質を担保できる
- 実装速度を高めることができる(工数削減)
- APIの利用者ではなくAPIの実装者がSpecificationを書くだけで良い
TypeScript向けのビルドパイプラインの設計
OASから実装に変換するフロートで、もっともシンプルなのは単純に変換することである。
OAS(yaml/json) --> TypeScript/JavaScript...etc
しかしながら、これだけでは実用に耐えられない。 APIのエンドポイントは複数合ったり、JavaScriptのライブラリとして提供するための体裁を整えたり、ドキュメントを出力するなど必要とされる状況やモノが多くある。
整理すると次のとおりだ。
複数のエンドポイントを要求
昨今はクライアントに対して1つのAPIエンドポイントになることはほとんどない。
api1.example.com, api2.example.com, ...etc
といった具合に使う場面はスケールしていくことを考慮する必要がある。
呼応して、APIのMock Serverもドメイン単位で構築されたほうが良いだろう。
JavaScriptライブラリとしての要求
- OAS(yaml)からTypeScriptの実装コードが吐き出せること
- 実装コードを出力するためのテンプレートが利用者によって変更が可能であること
- TypeScriptをビルドして、型定義ファイル(
.d.ts
)とJavaScriptのコードが吐き出されること - TypeScriptはesmoduleとcommonjs形式の両方でビルドされること
これらを整理して考案したビルドパイプラインを次に示す。詳細は後に説明する。
OASからTypeScriptを生成するための設計
ディレクトリ構成
ドメインがスケールできるように対応するために次のような構造をとった。
openapi-specification
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs # ドキュメントを生成場所
│ └── endpoints
│ └── api.example.com.json # OASのYAMLを1つのJSONにまとめたもの
├── endpoints # OASを記述する場所
│ ├── api.example.com # ドメイン単位でディレクトリを分ける
│ │ └── index.yml
│ └── api2.example.com # ドメイン単位でディレクトリを分ける
│ └── index.yml
├── lerna.json
├── lib # JavaScriptライブラリの生成場所
├── package.json
├── scripts # ビルド関連のスクリプトを配置
├── source # TypeScriptの生成場所
├── tsconfig.build.json
├── tsconfig.cjs.json
├── tsconfig.esm.json
├── tsconfig.json
└── yarn.lock
ビルドパイプライン
概略的な図を示すと次のようになる。
詳細を書く前にまずは技術選定から説明する必要がある。
技術選定
OpenAPI SpecificationからTypeScriptのコードと、ドキュメントを生成するためのツールとして利用したものは次の4つ。
- @apidevtools/swagger-cli
- api-spec-converter
swagger-typescript-codegen(2021/11/01現在使ってません)- @himenon/openapi-typescript-code-generator
- 用途:OpenAPI SchemaからTypeScriptの型定義/API Clientを生成する
- 採用理由
- 1ファイル書き出しが可能
- TypeScript ASTによる実装のためTypeScriptの実装を正確に出力する
- 型定義とApi Clientを分離することが可能
- Playgroundが存在する
- 内部で利用するAPI Client(fetch、axiosなど)がDI形式で実装できるため他のライブラリに依存しない
- npm
- GitHub
- Playground
- @stoplight/prism-cli
- swagger-ui
- 用途:OAS3.0の単一のJSONを読み取りドキュメントを生成する
- 採用理由
- 都度HTMLが生成されないのでHTML自体のビルド時間がゼロ
- OAS3.0のJSONのパスを指定するだけで良い
- GitHub
今回選ばなかったもの
- @openapitools/openapi-generator-cli
- typescript-fetch
- typescript-axios
- 不採用の理由
- 生成された実装のリクエストパラメーターの入力形式が関数の引数を増やす形式で使いづらい
- 実装がJava
- GitHub
- 不採用の理由
- redoc
パイプラインの説明
技術選定の内容を再度書き下す形式になってしまうが、次のように構成されている。
TypeScriptとドキュメントの生成
endpoints/{domain}/index.yml
をビルドのエントリーファイルとする@apidevtools/swagger-cli
を用いて、エントリーファイルをdocs/endpoints/{domain}.json
にOAS 3.0のJSONに変換するapi-spec-converter
を用いてOAS3.0をSwagger 2系に変換する- Swagger 2系に変換されたデータを
swagger-typescript-codegen
に与えることでTypeScriptの実装をsource/{domain}.ts
として出力する - Proxy Directory Pattern(※別記事参照)を用いてライブラリの体裁を整える
Mock Serverの起動
yarn run mock:server {domain}
実装サンプル
これらの設計を実装に落としたサンプルは以下のリポジトリにあります。
最後に
今回利用したswagger-typescript-codegen
は万能ではありません。
OASのoneOf
などのTypeScriptのUnion型で表現することが難しい場面もあります。
そのため、API Clientを自前で書く必要はなくなったが、コードジェネレーターをメンテナンスする必要は出てきます。