openapi-fetchでOpenAPIからコード生成してみた
フロントエンドでOpenAPI 3から型ファイルを生成したいとライブラリを探していたところopenapi-typescriptというリポジトリを見つけました。色々な方が記事で紹介していました。
しかし、openapi-typescriptばかりでopenapi-fetchがあまり紹介されていなかったので記事にしてみました。
openapi-typescriptのリポジトリでは以下の二つが紹介されています。
OpenAPIスキーマからTypeScriptの型のみを生成します
OpenAPIスキーマを取り込むタイプセーフなfetch client。重さは2KBで、実行時間は事実上ゼロです。React、Vue、Svelte、またはバニラ JS で動作します。
フロントエンドでのHttpClientはfetchを使う予定だったので、OpenAPIから型とfetchのClientまで生成してくれるopenapi-fetchを利用します。以下の表で示されるようにとっても速いです。
Library | Size (min) | “GET” request |
---|---|---|
openapi-fetch | 2 kB | 151k ops/s (fastest) |
openapi-typescript-fetch | 4 kB | 99k ops/s (1.4× slower) |
axios | 32 kB | 90k ops/s (1.6× slower) |
superagent | 55 kB | 42k ops/s (3× slower) |
openapi-typescript-codegen | 367 kB | 71k ops/s (2× slower) |
余談ですが、よく使われるaxiosですが、個人的にはあまりfetchと使用感は変わらないので、より軽量で速いfetchを使うのが良いと思います。
実装に関しては基本的にはREADMEに書かれている内容に沿ってやっていけばいけると思います。
具体的な実装
Setup
> npm i openapi-fetch
> npm i -D openapi-typescript typescript
型の生成
型の生成は以下コマンドで行います。
> npx openapi-typescript ./path/to/my/schema.yaml -o ./path/to/my/schema.d.ts
ただ、私はDockerでやりたかったので、docker runで実行する方法を取りました。以下はDockerfileとMakefileです
/container/schema/openapi/openapi-typescript/Dockerfile
FROM node:alpine
WORKDIR /app
RUN npm install -g openapi-typescript
ENTRYPOINT ["openapi-typescript"]
Makefile
.PHONY: gen-system-client-openapi
gen-system-client-openapi:
@rm -rf ./apps/system/client/src/generated/schema/openapi
@docker build -f ./container/schema/openapi/openapi-typescript/Dockerfile -t openapi-typescript-codegen-tmp .
@docker run --rm -v .:/app \
openapi-typescript-codegen-tmp \
/app/schema/api/system/openapi/openapi.yaml -o /app/apps/system/client/src/generated/schema/openapi/systemApi.ts
コード生成のたびにdocker run --rmでコンテナ終了時にコンテナを自動的に削除しています。コード生成自体が高速なので、あまり時間もかからず、ローカル環境が汚されないのでおすすめです。
HTTPクライアント作成
クライアントは以下のようになっており自動生成されたpathsを入れることで型が効くようになります。
import createClient from "openapi-fetch";
import type { paths } from "./api/v1"; // generated by openapi-typescript
const { GET, PUT } = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
インターセプトしたい場合
リクエストヘッダーにtokenを載せたかったので上記のクライアントにinterceptorを実装しました。clientのuseを使いmiddlewareを設定することができます。
以前の書き方
ライブラリのバージョンが更新されMiddlewareの書き方が少し変わりました。こちらは以前の書き方です
import createClient, { type Middleware } from "openapi-fetch"
import type { paths } from "~/generated/schema/openapi/systemApi"
import { firebaseAuth } from "~/infrastructure/firebase"
const fetchRequestInterceptor: Middleware = {
async onRequest(req, _options) {
if (firebaseAuth.currentUser === null) {
return req
}
const token = await firebaseAuth.currentUser.getIdToken()
if (!token) {
return req
}
req.headers.set("Authorization", `Bearer ${token}`)
return req
}
}
const fetchResponseInterceptor: Middleware = {
async onResponse(res, _options) {
if (res.status === 401) {
await firebaseAuth.signOut()
}
return res
}
}
export const openapiFetchClient = createClient<paths>({
baseUrl: import.meta.env.VITE_API_BASE_URL
})
openapiFetchClient.use(fetchRequestInterceptor)
openapiFetchClient.use(fetchResponseInterceptor)
export type SystemAPIClient = typeof openapiFetchClient
import createClient, { type Middleware } from "openapi-fetch"
import type { paths } from "~/generated/schema/openapi/systemApi"
import { firebaseAuth } from "~/infrastructure/firebase/setup"
const fetchRequestInterceptor: Middleware = {
async onRequest({ request }) {
if (firebaseAuth.currentUser === null) {
return request
}
const token = await firebaseAuth.currentUser.getIdToken()
if (!token) {
return request
}
request.headers.set("Authorization", `Bearer ${token}`)
return request
}
}
const fetchResponseInterceptor: Middleware = {
async onResponse({ response }) {
if (response.status === 401) {
await firebaseAuth.signOut()
}
return response
}
}
export const openapiFetchClient = createClient<paths>({
baseUrl: import.meta.env.VITE_API_BASE_URL
})
openapiFetchClient.use(fetchRequestInterceptor)
openapiFetchClient.use(fetchResponseInterceptor)
export type SystemAPIClient = typeof openapiFetchClient
レスポンスのハンドリング
ステータスコードに応じて処理を実行したかったので以下のような処理を書きました。
export const openapiFetchErrorInterpreter = (res: unknown): Error | null => {
if (res !== null && typeof res === "object" && "response" in res && (res as any).response instanceof Response) {
const r = res as {
data?: undefined
error?: { code?: number; message?: string }
response: Response
}
return convertToErrorByStatusCode(r.response.status, r.error?.message)
}
return null
}
responseで返ってくるerrorをそのまま渡します。
const apiRequest = async () => {
const response = await GET("/blogposts/{post_id}", {
params: {
path: { post_id: "123" }
}
})
if (response.error) {
throw openapiFetchErrorInterpreter(response)
}
return response.data
}
まとめ
軽量でとてもシンプルなので使いやすいライブラリでした。今後もこれを使っていこうと思います。
Discussion