Open Api Generator(typescript-fetch)で自動生成したApiClientを使ったリクエストの前後に処理を追加する
Open Api Generatorとは
OpenAPI Generator allows generation of API client libraries (SDK > generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported)
https://github.com/OpenAPITools/openapi-generator#overview より引用
とのこと。つまり、特定の言語に依存しないAPI記述仕様であるOpenAPI Specから各言語で利用するためのクライアントライブラリや、スタブサーバー、ドキュメントや設定を自動生成してくれるツールです。
何が嬉しいの?
OpenAPI Spec(以下OAS)はAPI記述の標準仕様です。
そのため、OASに基づいて作成された成果物を用いることで、APIの仕様について他者とコミュニケーションを取る際のコストを軽減することが期待できます。そして、作成された成果物からクライアントライブラリ等を自動生成することで、実装ミスの軽減したり、仕様と実装の乖離を防ぐ効果があります。
typescript-fetch Generator
typescript-fetch GeneratorはfetchAPIを用いたTypeScriptのクライアントライブラリのGeneratorです。類似のGeneratorとしてtypescript-axiosなどがあります。同じTypeScript用のGeneratorであっても、ものによってconfig可能な値や生成されるコードに違いがあるため、自身のプロジェクトが採用している技術に応じてGeneratorを選択すると良いでしょう。
今回はTypeScript×fetchAPIを使っているプロジェクトへ導入するという前提で、typescript-fetch Generator
の簡単な使い方について解説していきます。
コードの生成
実際にツールを使ってコードを生成する方法はいくつかあるので公式の手順を参照してみてください。
今回はDockerを使った方法で生成してみます。
docker run --rm -v "${PWD}:/local" \
openapitools/openapi-generator-cli:v6.0.0 generate \
-i /local/openapi.yaml \
-g typescript-fetch \
-o /local/dist/target
上記のコマンドを実行すると、targetディレクトリ以下にファイルが生成されます。
なお、最新の安定版はv6.2.1ですが、生成されたコードが型エラーになってしまったため、v6.0.0を使用しています。
生成に使用したOpenAPIドキュメントはこちら
openapi: 3.0.0
info:
version: 1.0.0
title: Example
paths:
/user:
get:
operationId: getUser
tags:
- User
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/User"
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
使い方
生成されたAPIクライアントのメソッドを実行すれば型付けされたレスポンスを得ることができます。便利ですね。
Middlewareの実装
APIリクエストといえば、処理の前後に認証や後処理などの共通処理を挟み込むことがよくあります。
typescript-fetchには、それを叶えるMiddleware
という仕組みが用意されています。
詳しくは生成されたruntime.ts
というソースを読んでいただきたいのですが、UserApiの継承元としてBaseApiというクラスが定義されており、コンストラクタやwithMiddleware
メソッドでMiddlewareという型を受け取れます。
型定義はこんな感じ
export interface Middleware {
pre?(context: RequestContext): Promise<FetchParams | void>;
post?(context: ResponseContext): Promise<Response | void>;
}
このMiddleware型を満たすようなインスタンスをUserApiに設定してあげると、pre
関数でリクエストの前に、post
関数でリクエストの後に実施したい処理を挟み込むことができます。
const middleware: Middleware = {
pre: async (context: RequestContext) => {
console.log("pre function")
return {
init: context.init,
url: context.url
}
},
post: async (context: ResponseContext) => {
console.log("post function")
return Promise.resolve(context.response)
}
}
// 初期化時に渡す
const user = await (new UserApi(new Configuration({ middleware: [middleware] })).getUser())
// 後から設定もできる
const user = await (new UserApi().withMiddleware(middleware).getUser())
簡単ですね。(このあたりの話がドキュメントに書かれていると嬉しいのですが、私には見つけられませんでした。誰か見つけたら教えて下さい。)
さらに便利に
こんな感じすれば、各APIで共通処理を使いつつ必要なときにoverrideできて良さそう?(未検証)
const initClient = <T extends BaseAPI>(api: T, middleware?: Middleware): ApiClient<T> => {
const defaultMiddleware = {} as Middleware // デフォルト動作を規定
return middleware ? api.withMiddleware(middleware) : api.withMiddleware(defaultMiddleware)
}
type ApiClient<T extends BaseAPI> = {
[P in keyof T]: T[P]
}
余談
生成されるコードのセミコロンの有無に一貫性がなく、夜も眠れなかったので、PR出したら無事マージされた。祝コントリビュート。