Closed13
OpenAPI仕様書からフェッチクライアントを自動生成する
背景
- 既存Vueアプリで、リクエスト・レスポンスに型がない
- Reactリプレイスにあたり型安全性を確保したい
- 対象APIが300本以上あり、手動で型定義をしていられない
前提
- OpenAPI仕様書がSSOT(Single Source of Truth)として存在すること
自動生成したいもの
- リクエスト・レスポンスパラメーターのTypeScript型(必須)
- 型安全なAPIクライアント(必須)
- 型安全なカスタムフック群
- 型安全なモックハンドラ群
比較
openapi-typescript
openapi-typescriptで型生成し、openapi-fetchに食わせて型安全・軽量・高速なクライアントを生成
axiosにすら依存していなくて非常に薄い
orval
react-queryのカスタムフック、mswのハンドラまで生成してくれる全部盛り
npmtrends
24/03/13時点
openapi-fetch × react-query
型付きクライアント
lib/api/index.ts
import createClient, { type Middleware } from "openapi-fetch";
import type { paths } from "./v1";
const throwOnError: Middleware = {
async onResponse(res) {
if (res.status >= 400) {
const body = res.headers.get("content-type")?.includes("json")
? await res.clone().json()
: await res.clone().text();
throw new Error(body);
}
return undefined;
},
};
const client = createClient<paths>({ baseUrl: "https://catfact.ninja/" });
client.use(throwOnError);
export default client;
APIアクセスフック
hooks/queries.ts
import { useQuery } from "@tanstack/react-query";
import type { ParamsOption, RequestBodyOption } from "openapi-fetch";
import type { paths } from "../lib/api/v1";
import client from "../lib/api";
type UseQueryOptions<T> = ParamsOption<T> &
RequestBodyOption<T> & {
// add your custom options here
reactQuery?: {
enabled: boolean; // Note: React Query type’s inference is difficult to apply automatically, hence manual option passing here
// add other React Query options as needed
};
};
// paths
const GET_FACT = "/fact";
export function getFact({
params,
body,
reactQuery,
}: UseQueryOptions<paths[typeof GET_FACT]["get"]>) {
return useQuery({
...reactQuery,
queryKey: [
GET_FACT,
// add any other hook dependencies here
],
queryFn: async ({ signal }) => {
const { data } = await client.GET(GET_FACT, {
params,
// body - isn’t used for GET, but needed for other request types
signal, // allows React Query to cancel request
});
return data;
// Note: Error throwing handled automatically via middleware
},
});
}
きちんと型生成するための推奨事項
① ルール
- オペレーションIDがユニークであること(operation-operationId-unique)
- オペレーション内でパラメーターが重複しないこと(operation-parameters-unique)
- パス定義にはクエリ文字列を含めないこと(path-not-include-query)
- 仕様書のバージョンは
3.0
or3.1
であること(spec)
② JSでもsnake_case->calmeCaseの変換は非推奨
③ tsconfigのcompilerOptions.noUncheckedIndexedAccessを有効化
④ 可能な限り具体的に
-
additionalProperties
の追加を推奨 - タプル型は内容をスキーマで表現しておくことを推奨
⑤ oneOfを単独で使用する
所感
openapi-typescript
- もっぱら「OpenAPI to TS」の型生成に専念しており好き
- 正しい型を生成するためのスタイルガイドまである
- これ自体がコミュニケーションIFになる
- 型生成が破綻したらその上に乗っているカスタムフックは結局死んでしまうわけで、そこだけに徹底してくれている安心感
- npm trendsでも圧倒的
orval
- カスタムフックまで自動生成してくれる
- 自動生成されたreact-queryのカスタムフックも、重要なプラクティスを満たした良いコードだった
- 一方で、react-query, swrなど複数のフェッチフックに対応し、かつvue, angular, svelteにまで対応しているいわゆる全部盛り
- 「一つのことをうまくやれ」に強烈に反しているような
- カスタムフック欲しさに、根幹となる型システムの生成までこの子に任せていいのか、本能レベルで恐怖を感じる
このスクラップは2024/03/13にクローズされました