OpenAPI から TypeScript の型を生成して API をいい感じで呼びたかった
結論
結局 openapi-typescript + openapi-fetchを使い、トークン切り替えはカスタム fetchを書いてあげることにした
openapi-generatorへの課題感
Nuxt2からNuxt3に書き換えるにあたって、axios から、 OAS を利用したコード生成に切り替え、4つ中2つのサイトについては既に対応を完了した。
対応済みの2つのサイトについては、openapi-generatorを用いてAPI呼び出しコードを生成している。
しかし、以下のような気になる点があった。
- 生成コードが冗長でバンドルサイズ・パフォーマンスに懸念がある。classで書かれているためメソッド単位ではtreeshakingできない。
- axiosなどで書かれたコードから書き換える場合、operationId を元にした関数名に書き換える必要がある。単純にパターン置換などでは既存のaxios利用コードからの書き換えができない。
上記は、開発上は楽な面もあったので、一概に悪いといえないが、残りの2サイトの書き換えをするにあたってより適切な手法を調査・検討したいと思った。
技術選定における要点
- バンドルサイズへの影響が少ない
- 処理が軽い
- axiosからの書き換えが容易
- TypeScriptによる補完が、入力(URL, query, path etc) や 出力 (Response body) に効くこと
- onRequest, onResponseなどのinterceptor用のコールバックが設定できる
- トークンの切り替えなどを利用コード側から隠蔽して行えるようにしたいため
openapi-typescript と openapi-fetch
思想が非常によいなと思っていて、特に snake_case の利用に関する記述については、今まで複数のプロジェクトで snake_case から camelCase に変換しており、作業としてもブラウザの動作としても無駄だったなと思わされた。
他方、非常に薄く作られているがために他のfetchライブラリでは必ずあるような機能がない。
例えば、middlewareとして、onResponseを設定できるが、このタイミングでRequestを取得できないため、何を送信したかを知ることができない。
対応
もしやるとすると カスタムfetchを実装してあげることになる
nuxt-open-fetch
どうせカスタムするのであれば、Nuxt3でデフォルトで$fetchとして利用できるようになっているofetchをOpenAPIの型がつかえるようにすれば良いのではと思い調べてみると、ほぼ同じことをしているライブラリが存在した。
しかし試してみると、parameters
のないoperationにおいてエラーが発生する
生成された型
paths:{
'/certification/company/login': {
/**
* ReDS: 企業アカウント認証
* @description 企業アカウントのログイン処理
*/
post: operations['authenticate-company-account']
}
},
operations:{
'authenticate-company-account': {
requestBody?: {
content: {
'application/json': {
email?: string
password?: string
}
}
}
responses: {
/** @description OK */
200: {
headers: {}
content: {
'application/json': {
/** @description 企業認証トークン */
//省略
}
}
利用コード
const data = await $apiFetch('/certification/company/login', {
method: 'POST',
body: { email, password },
})
エラー
Argument of type '{ method: "POST"; body: { email: string; password: string; }; }' is not assignable to parameter of type 'OpenFetchOptions<"POST", "post", { post: { requestBody?: { content: { 'application/json': { email?: string | undefined; password?: string | undefined; }; }; } | undefined; responses: { 200: { headers: {}; content: { ...; }; }; 400: { ...; }; }; }; }, { ...; }>'.
Type '{ method: "POST"; body: { email: string; password: string; }; }' is not assignable to type 'Record<string, never>'.
Property 'method' is incompatible with index signature.
Type 'string' is not assignable to type 'never'.ts(2345)
エラーの原因
ParamsOption がparameters
が operation にないと、Record<string, never>
を返すようになっており、これを options 全体と union していて、プロパティのkey が全て never になっているように見える。
parameters
ないというのは普通にありえることで、まちがっているのではないかなと思っている。
export type ParamsOption<T> = T extends {
parameters?: any;
query?: any;
} ? T['parameters'] : Record<string, never>;
対応
そもそもoperation直下のparamsをofetchの引数としてぶちこんでもheaderなどに値がわたらないなど、普通に使うにあたって困りそうなので一旦候補からはずす。
検討
- ofetch使うために自前で型書くのもコストがかかりそう
- カスタムfetchを書くのは比較的想像しやすい
カスタムfetchとりあえず書いてみる
カスタムfetchを使った openapi-fetchのクライアント生成
headerまわりもうちょっと綺麗にできそうだけど一旦書いた
const addHeader = (init: RequestInit, key: string, value: string) => {
if (init.headers) {
;(init.headers as Headers).set(key, value)
} else {
init.headers = new Headers({
[key]: value,
})
}
}
// ベースになるカスタムfetch
// 共通で必要なheaderを追加する
const apiFetchBase = (
request: string | URL | globalThis.Request,
init?: RequestInit
): Promise<Response> => {
if (!init) {
init = {}
}
addHeader(init, 'Content-Type', 'application/json')
addHeader(init, 'X-API-Token', apiToken)
return fetch(request, init)
}
// トークンの追加・トークンのローテーションを行うカスタムfetch
const apiFetch = async (
request: string | URL | globalThis.Request,
init?: RequestInit
): Promise<Response> => {
if (!init) {
init = {}
}
if (authToken.value) {
addHeader(init, 'X-ADMIN-Token', authToken.value)
}
let response = await apiFetchBase(request, init)
if (!response.ok) {
const responseData = await response.clone().json()
if (responseData.status === 407) {
const tokenResponse = await apiFetchBase(
baseUrl + '/certification/company/reissue'
)
const tokenResult = await tokenResponse.json()
setAuthToken(tokenResult.admin_token)
addHeader(init, 'X-API-Token', tokenResult.admin_token)
response = await apiFetchBase(request, init)
}
}
return response
}
// openapi-fetchのclient生成
const api = createClient<paths>({
baseUrl,
fetch: apiFetch,
})