orvalでOpenAPIからReact Queryフックを自動生成する

スポンサーリンク

orvalでOpenAPIからReact Queryフックを自動生成する

はじめに

OpenAPIで定義したAPIをReactから呼び出すとき、useQueryuseMutation を毎回手書きしていませんか?

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・共通化まで」を参照してください。