orvalでOpenAPIからReact Queryフックを自動生成する
はじめに
OpenAPIで定義したAPIをReactから呼び出すとき、useQuery や useMutation を毎回手書きしていませんか?
orval を使うとOpenAPI仕様からTypeScriptの型定義・React Queryフック・APIクライアントをまとめて自動生成できます。APIの変更に追従しやすく、Next.js + React Queryの組み合わせで特に人気のあるツールです。
型定義のみの生成は「OpenAPIからTypeScript型定義とfetchクライアントを自動生成する」を参照してください。
インストール
npm install -D orval npm install @tanstack/react-query
設定ファイルを作成する
プロジェクトルートに orval.config.ts を作成します。
import { defineConfig } from "orval"; export default defineConfig({ api: { input: "./openapi.yaml", output: { mode: "tags-split", target: "./src/api/generated", schemas: "./src/api/model", client: "react-query", httpClient: "fetch", override: { mutator: { path: "./src/lib/fetcher.ts", name: "customFetch", }, }, }, }, });
設定項目の説明
| キー | 説明 |
|---|---|
input |
OpenAPI仕様ファイルのパス |
output.target |
生成ファイルの出力先 |
output.schemas |
型定義の出力先 |
output.client |
react-query / swr / axios など |
output.mode |
tags-split でタグごとにファイルを分割 |
output.httpClient |
fetch / axios |
カスタムfetcherを作成する
認証ヘッダーやベースURLなど共通の設定を差し込むためのfetcherを作ります。
// src/lib/fetcher.ts export const customFetch = async <T>( url: string, options: RequestInit, ): Promise<T> => { const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; const response = await fetch(`${baseUrl}${url}`, { ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json(); };
認証トークンを付与する場合はここで追加します。
export const customFetch = async <T>( url: string, options: RequestInit, ): Promise<T> => { const token = localStorage.getItem("token"); const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}${url}`, { ...options, headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...options.headers, }, }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json(); };
フックを生成する
npx orval
以下のようなOpenAPI仕様があるとします。
paths: /users: get: operationId: getUsers tags: [users] responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/User' post: operationId: createUser tags: [users] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateUserInput' responses: '201': content: application/json: schema: $ref: '#/components/schemas/User' /users/{id}: get: operationId: getUsersId tags: [users] parameters: - name: id in: path required: true schema: type: integer responses: '200': content: application/json: schema: $ref: '#/components/schemas/User'
tags-split モードでは src/api/generated/users.ts が生成されます。
// 生成されるフック(イメージ) export const useGetUsers = ( options?: UseQueryOptions<User[], Error>, ) => { return useQuery({ queryKey: ["getUsers"], queryFn: () => customFetch<User[]>("/users", { method: "GET" }), ...options, }); }; export const useGetUsersId = ( id: number, options?: UseQueryOptions<User, Error>, ) => { return useQuery({ queryKey: ["getUsersId", id], queryFn: () => customFetch<User>(`/users/${id}`, { method: "GET" }), ...options, }); }; export const useCreateUser = ( options?: UseMutationOptions<User, Error, CreateUserInput>, ) => { return useMutation({ mutationFn: (body: CreateUserInput) => customFetch<User>("/users", { method: "POST", body: JSON.stringify(body), }), ...options, }); };
生成したフックを使う
// app/users/page.tsx "use client"; import { useGetUsers } from "@/api/generated/users"; export default function UsersPage() { const { data: users, isLoading, error } = useGetUsers(); if (isLoading) return <p>読み込み中...</p>; if (error) return <p>エラーが発生しました</p>; return ( <ul> {users?.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }
// app/users/new/page.tsx "use client"; import { useCreateUser } from "@/api/generated/users"; import { useQueryClient } from "@tanstack/react-query"; export default function NewUserPage() { const queryClient = useQueryClient(); const { mutate: createUser, isPending } = useCreateUser({ onSuccess: () => { // ユーザー一覧を再取得 queryClient.invalidateQueries({ queryKey: ["getUsers"] }); }, }); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); createUser({ name: formData.get("name") as string, email: formData.get("email") as string, }); }; return ( <form onSubmit={handleSubmit}> <input name="name" placeholder="名前" /> <input name="email" placeholder="メールアドレス" /> <button type="submit" disabled={isPending}> {isPending ? "作成中..." : "作成"} </button> </form> ); }
SWRを使う場合
React Queryではなく SWR を使う場合は設定を変えるだけです。
npm install swr
// orval.config.ts export default defineConfig({ api: { input: "./openapi.yaml", output: { client: "swr", // ここを変える // ... }, }, });
npm scriptsに組み込む
{ "scripts": { "generate:api": "orval" } }
npm run generate:api
ファイルを監視して変更のたびに自動生成することもできます。
{ "scripts": { "generate:api": "orval", "generate:api:watch": "orval --watch" } }
openapi-typescriptとの比較
| openapi-typescript + openapi-fetch | orval | |
|---|---|---|
| 生成物 | 型定義 + fetchクライアント | 型定義 + React Query/SWRフック |
| React Query統合 | 手書きが必要 | 自動生成 |
| カスタマイズ | シンプル | 設定項目が多い |
| 向いている場面 | 型安全なfetchだけ欲しいとき | Reactで本格的に使うとき |
Reactプロジェクトで useQuery / useMutation まで自動化したい場合はorval、型定義とfetchクライアントだけで十分な場合はopenapi-typescriptが向いています。
まとめ
- orvalはOpenAPI仕様からReact Query / SWRフックを丸ごと自動生成できる
orval.config.tsでカスタムfetcherを差し込むことで認証などの共通処理を一元管理できるtags-splitモードでAPIタグごとにファイルが分割され、大規模プロジェクトでも管理しやすい- API仕様の変更は
npm run generate:apiの再実行だけでフロントエンドに反映できる
OpenAPIのスキーマ設定については「OpenAPIスキーマ設定チートシート:nullable・enum・共通化まで」を参照してください。