← プログラミングページの先頭へ

Orval: OpenAPI TypeScript Client コード生成

OrvalはOpenAPI Schemaを元にTypeScript API Clientを自動生成するツールです。

Django + DRF + React プロジェクトの場合、Orval は自動的に生成できます。

  • TypeScript リクエストとレスポンスのタイプ。
  • HTTPリクエスト関数。
  • TanStack Query Hooks。
  • Query KeyとQuery Options。
  • Mutation HooksとMutation Options。
  • Mock データとフロントエンド テスト用の MSW Handler。

基本的なワークフロー:

Django / DRF
     ↓
drf-spectacular
     ↓
OpenAPI Schema
     ↓
    Orval
     │
     ├── TypeScript Types
     ├── Request Functions
     ├── Query Keys
     ├── Query Options
     ├── Mutation Options
     └── React Query Hooks
               ↓
         React Components

Orval の主な価値は、API タイプと要求コードを手動で保守する労力を軽減し、フロントエンドとバックエンドのインターフェイスの一貫性を保つことです。

1. インストールと基本設定

インストール:

pnpm add -D orval
pnpm add axios @tanstack/react-query

通常、プロジェクトのルート ディレクトリに作成されます。

orval.config.ts

基本構成

import { defineConfig } from "orval";

export default defineConfig({
  api: {
    input: {
      target: "./openapi.yaml",
    },

    output: {
      target: "./src/api/generated/index.ts",
      schemas: "./src/api/generated/model",
      client: "react-query",
      httpClient: "axios",
      mode: "tags-split",
    },
  },
});

主な構成:

構成機能
input.targetOpenAPI ファイルパスまたは URL
output.target生成されたAPIコードの出力場所
output.schemasTypeScript Model 出力ディレクトリ
output.client生成されたクライアントタイプ
output.httpClientHTTPリクエストの実装
output.mode生成されたファイルの編成方法
output.mockMockを生成するかどうか

clientとhttpClientの違い

client: "react-query",
httpClient: "axios",

意味:

  • react-query: TanStack Query Hooks およびヘルパー関数を生成します。
  • axios: リクエスト関数は Axios を使用して HTTP リクエストを送信します。

二人は異なる問題を解決します。

2. コマンドの生成

pnpm exec orval

package.json に参加できます:

{
  "scripts": {
    "api:generate": "orval",
    "build": "tsc -b && vite build"
  }
}

実行:

pnpm api:generate

Orval は OpenAPI Schema を読み取り、対応する TypeScript コードを生成します。

再生成時に変更内容が上書きされるため、通常、メイクファイルを直接編集することはお勧めできません。

3. operationId

通常、OpenAPI の operationId は、生成される関数の名前を決定します。

たとえば:

paths:
  /api/v1/user/profile/:
    get:
      operationId: userProfileRetrieve
      responses:
        "200":
          description: Success

以下が生成される可能性があります:

userProfileRetrieve();

useUserProfileRetrieve();

getUserProfileRetrieveQueryKey();

getUserProfileRetrieveQueryOptions();

具体的な機能名と使用できる補助機能はOrvalのバージョンと構成によって異なります。

命名原則

Django では安定した operationId を使用することをお勧めします。

@extend_schema(
    operation_id="getUserProfile",
)

これにより、次のものが生成される可能性があります。

getUserProfile();
useGetUserProfile();
getGetUserProfileQueryKey();

注: operationId を変更すると、関連するすべての生成関数の名前が変更される可能性があり、フロントエンド TypeScript コンパイル エラーが発生する可能性があります。

したがって、operationId は API Client の公的契約の一部と見なされるべきです。

4.ノーマルRequest Function

バックエンドインターフェイスを想定すると:

GET /api/v1/user/profile/

Orval は次のようなものを生成できます。

export const userProfileRetrieve = (
  signal?: AbortSignal,
): Promise<UserProfile> => {
  return axiosInstance<UserProfile>({
    url: "/api/v1/user/profile/",
    method: "GET",
    signal,
  });
};

ここでは、axiosInstance<T> がカスタム Mutator であると想定され、Promise<T> が返されます。ここで、T は応答本体タイプです。

リクエストプロセス:

React Component
       ↓
Request Function
       ↓
Axios Instance
       ↓
HTTP Request
       ↓
Django API

Request Function の責任

  • API URL をビルドします。
  • HTTP Methodを指定してください。
  • Query Parameters を通過します。
  • Request Body を通過します。
  • TypeScript 戻り値の型を指定します。
  • HTTP Client に電話してください。

通常、それ自体は TanStack Query キャッシュを担当しません。

5.TanStack Query統合

構成:

client: "react-query",

Orval は、React の TanStack Query コードを生成します。

その主な構造は次のとおりです。

Request Function
       │
       ▼
    QueryFn
       │
       ├── Query Key
       │
       └── Query Options
                │
                ▼
             useQuery
                │
                ▼
          React Component

注: queryKey、queryFn、および queryOptions は、一緒になって Query を形成する構成関係であり、順番に実行されるステップを要求するものではありません。

6. Query Key

Query Key は、キャッシュされたデータを識別するために TanStack Query によって使用される一意のキーです。

Orval は通常、Query Key ファクトリーを生成します。

export const getUserProfileRetrieveQueryKey = () => {
  return ["/api/v1/user/profile/"] as const;
};

パラメータのあるインターフェースの場合:

GET /api/v1/pois?city=1&page=2

Query Key 異なるパラメータを区別する必要があります。

たとえば:

["/api/v1/pois", { city: 1, page: 2 }];

と:

["/api/v1/pois", { city: 2, page: 2 }];

別のキャッシュ エントリである必要があります。

Query Keyの主な目的

  • ID キャッシュ。
  • 指定されたキャッシュを読み取ります。
  • 指定されたキャッシュを更新します。
  • キャッシュを削除します。
  • キャッシュを無効化します。
  • クエリの再フェッチを制御します。

たとえば:

queryClient.invalidateQueries({
  queryKey: getUserProfileRetrieveQueryKey(),
});

これにより、一致する Query が stale としてマークされ、デフォルト構成でアクティブな Query のデータが再フェッチされます。

7. Query Options

Orval は以下を生成する可能性があります:

export const getUserProfileRetrieveQueryOptions = () => {
  return {
    queryKey: getUserProfileRetrieveQueryKey(),

    queryFn: ({ signal }: { signal?: AbortSignal }) =>
      userProfileRetrieve(signal),
  };
};

実際に生成されるコードには、複雑な TypeScript ジェネリック、パラメーター、オーバーロードも含まれます。

Query Optionsの役割

TanStack Query の構成を 1 つのオブジェクトに収集します。

{
  queryKey,
  queryFn,
  staleTime,
  gcTime,
  enabled,
  retry,
  refetchOnWindowFocus,
}

たとえば、React では次のようになります。

const options = getUserProfileRetrieveQueryOptions();

const result = useQuery({
  ...options,
  staleTime: 60_000,
});

staleTime は、データが新しいとみなされる時間を表します。

gcTime は、非アクティブな Query のキャッシュを保持できる期間を示します。

注: これら 2 つのパラメーターは TanStack Query によって実装され、Orval は対応する構成コードを生成するだけです。

8. useQuery Hook

Orval はさらに生成できます。

export const useUserProfileRetrieve = () => {
  return useQuery(getUserProfileRetrieveQueryOptions());
};

実際のコードは通常、追加の Query Options および Axios Options をサポートする必要があります。

使用する

function UserProfile() {
  const { data, isPending, isError, error } = useUserProfileRetrieve();

  if (isPending) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>{error.message}</div>;
  }

  return <div>{data.name}</div>;
}

TanStack Query は以下の管理を担当します。

  • data: データを取得しました。
  • isPending: 初期データ待ちかどうか。
  • isFetching: リクエストが実行中かどうか。
  • isError: エラーが発生したかどうか。
  • error: エラー オブジェクト。
  • refetch: 手動で再リクエストします。

マニュアル Query および Orval Hook

Orval は使用しないでください。

useQuery({
  queryKey: ["user", "profile"],
  queryFn: getUserProfile,
});

Orval を使用します。

useUserProfileRetrieve();

後者により、リクエスト関数、Query Key、および型定義を繰り返し記述する労力が軽減されます。

9. Mutation

Mutation は、次のような副作用のある操作を実行するためによく使用されます。

  • POST: 作成されました。
  • PUT: 交換品。
  • PATCH: 変更されました。
  • DELETE: 削除します。

仮定:

PATCH /api/v1/user/profile/

Orval は次のコードを生成する場合があります。

Request Function

export const userProfilePartialUpdate = (
  data: PatchedUserProfile,
): Promise<UserProfile> => {
  return axiosInstance<UserProfile>({
    url: "/api/v1/user/profile/",
    method: "PATCH",
    data,
  });
};

Mutation Options

export const getUserProfilePartialUpdateMutationOptions = () => {
  return {
    mutationFn: (variables: { data: PatchedUserProfile }) =>
      userProfilePartialUpdate(variables.data),
  };
};

Mutation Hook

export const useUserProfilePartialUpdate = () => {
  return useMutation(getUserProfilePartialUpdateMutationOptions());
};

全体的な関係:

useMutation
     ↓
Mutation Options
     ↓
mutationFn
     ↓
Request Function
     ↓
Axios
     ↓
Django API

10. Mutationを使用する

function ProfileEditor() {
  const mutation = useUserProfilePartialUpdate();

  const handleUpdate = () => {
    mutation.mutate({
      data: {
        name: "Alice",
      },
    });
  };

  return (
    <button onClick={handleUpdate} disabled={mutation.isPending}>
      Update
    </button>
  );
}

mutate および mutateAsync

mutation.mutate({
  data: { name: "Alice" },
});

mutate() は Mutation をトリガーするために使用され、結果は通常コールバックを通じて処理されます。

そして:

await mutation.mutateAsync({
  data: { name: "Alice" },
});

mutateAsync() は Promise を返します。これは、操作が完了するまで待つ必要があるシナリオに適しています。

Mutationステータス

プロパティ意味
isIdleまだ実行されていません
isPending実行中
isSuccess成功
isError失敗しました
dataデータが正常に返されました
errorエラーメッセージ
reset()Mutation状態をリセット

11. Mutationとキャッシュの無効化

Mutation がサーバー データを変更すると、キャッシュされた Query データの有効期限が切れる可能性があります。

たとえば:

GET User Profile
       ↓
Cache User Data
       ↓
PATCH User Profile
       ↓
Server Data Changed
       ↓
Cached Data Outdated

したがって、通常は Mutation が成功した後、関連する Query を無効にする必要があります。

invalidateQueries

const queryClient = useQueryClient();

const mutation = useUserProfilePartialUpdate({
  mutation: {
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: getUserProfileRetrieveQueryKey(),
      });
    },
  },
});

ここではOrval Hookの一般的な設定方法を示します。特定のパラメータ構造は、生成されたファイルに基づく必要があります。

なぜ invalidateQueries が必要なのでしょうか?

MutationとQueryのキャッシュは独立して管理されているためです。

実行:

useUserProfilePartialUpdate();

必ずしも次のことを意味するわけではありません。

useUserProfileRetrieve();

キャッシュは自動的に更新されます。

Orval はキャッシュの無効化に関連する動作を生成する構成をサポートしていますが、自動的に無効化するかどうかは構成に依存し、デフォルトではすべての Mutation が関連する Query を自動的に更新するとは想定できません。

12. setQueryData

再リクエストに加えて、キャッシュを直接変更することもできます。

queryClient.setQueryData(getUserProfileRetrieveQueryKey(), updatedProfile);

Mutation が最新の完全なリソースを返した状況に適しています。

invalidateQueries および setQueryData

方法機能
invalidateQueriesキャッシュを無効としてマークし、条件付きで取得します
setQueryDataキャッシュに直接書き込む
removeQueriesキャッシュエントリを削除する
resetQueriesQuery状態をリセット

推奨される原則:

  • Mutation は成功ステータスのみを返します。通常は invalidateQueries を使用します。
  • Mutation は完全で信頼性の高い最新データを返します。setQueryData を検討してください。
  • 複数のクエリは同じリソースに依存します。関連するキャッシュの一貫性を同時に考慮する必要があります。

13. Axios Mutator

Mutator は、Orval の非常に重要な拡張メカニズムです。

これにより、生成されたすべての関数がデフォルトの Axios を直接呼び出すのではなく、プロジェクト独自の Axios インスタンスを使用できるようになります。

orval.config.ts

export default defineConfig({
  api: {
    input: {
      target: "./openapi.yaml",
    },

    output: {
      target: "./src/api/generated/index.ts",
      client: "react-query",
      httpClient: "axios",

      override: {
        mutator: {
          path: "./src/api/axios-instance.ts",
          name: "axiosInstance",
        },
      },
    },
  },
});

カスタムAxios Instance

import Axios, { type AxiosRequestConfig, type AxiosError } from "axios";

const client = Axios.create({
  baseURL: "/",
  withCredentials: true,
  timeout: 30_000,
});

export const axiosInstance = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig,
): Promise<T> => {
  return client({
    ...config,
    ...options,
  }).then(({ data }) => data);
};

export type ErrorType<E> = AxiosError<E>;

export type BodyType<T> = T;

ここでの withCredentials: true は、クロスオリジン Cookie リクエストには便利ですが、CSRF 保護は自動的に実装されません。

実際の通話:

Orval Generated Function
           ↓
      axiosInstance()
           ↓
       Axios Client
           ↓
    Request Interceptor
           ↓
       Django API
           ↓
    Response Interceptor
           ↓
       Response Data

Mutatorの目的

  • Base URLを設定してください。
  • Cookieをキャリーします。
  • Authorization Headerを追加します。
  • CSRF を処理します。
  • タイムアウト設定を統合しました。
  • API エラーを処理します。
  • 応答データを変換します。
  • リクエストのキャンセルを処理します。

14. Axios Interceptors

インターセプターはカスタム Axios Instance で構成できます。

Request Interceptor

client.interceptors.request.use((config) => {
  config.headers.set("X-App-Version", "1.0.0");

  return config;
});

パブリック Header を追加するために使用できます。

Response Interceptor

client.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle authentication error
    }

    return Promise.reject(error);
  },
);

注: 401 が常にログイン ページへのジャンプをトリガーするわけではありません。たとえば、一部のインターフェイスでは匿名アクセスが許可されている場合や、ログイン検証が実行されている場合があります。

グローバル エラー処理では、ビジネス コンテキストを考慮する必要があります。

15. AbortSignal: リクエストのキャンセル

Orval によって生成された Query Function は、通常、TanStack Query によって提供された AbortSignal を Axios に渡すことができます。

たとえば:

queryFn: ({ signal }) => userProfileRetrieve(signal);

リクエスト機能:

axiosInstance({
  url: "/api/v1/user/profile/",
  method: "GET",
  signal,
});

TanStack Query がリクエストをキャンセルすると、Axios はこの Signal を使用して、対応する HTTP リクエストを中止できます。

一般的なシナリオ:

  • ユーザーがページを離れます。
  • クエリ条件が変更されます。
  • Queryを手動でキャンセルしてください。
  • リクエスト結果は不要になりました。

注: ブラウザは HTTP リクエストを中止しますが、これはサーバーがリクエストの処理を停止したことを保証するものではありません。

16. TypeScript型生成

Orval は、OpenAPI Schema に基づいてタイプ TypeScript を生成します。

たとえば:

UserProfile:
  type: object
  required:
    - id
    - name
  properties:
    id:
      type: integer
    name:
      type: string
    avatar:
      type: string
      nullable: true

以下が生成される可能性があります:

export interface UserProfile {
  id: number;
  name: string;
  avatar?: string | null;
}

required および nullable

この 2 つは異なります。

SchemaTypeScriptの意味
requiredプロパティが存在する必要があります
requiredではありません属性は省略可能
nullable属性値は null にすることができます。

たとえば:

name: string;

必須であることを示しますが、null は許可されません。

name?: string | null;

属性が省略できることを示します。null も許可されます。

型生成の制限

Orval は、Schema に基づいて静的型を生成し、実行時データを自動的に検証しません。

たとえば:

const profile: UserProfile = await userProfileRetrieve();

TypeScript は、API 関数の宣言された戻り値の型を信頼しますが、TypeScript 自体は、サーバーが実際に間違ったデータ構造を返したかどうかを実行時に検出できません。

実行時検証が必要な場合は、Zod などのツールを使用するか、バックエンド Serializer に依存して出力構造を確認できます。

17. Output Mode

Orval は、生成されたファイルを整理するさまざまな方法をサポートしています。

よくあるパターン:

Mode機能
single生成されたコンテンツを 1 つのファイルに集約する
splitAPI を Schema から分離する
tagsTags に従ったグループ化
tags-splitTags に基づいてファイルをグループ化および分割する

たとえば:

output: {
  mode: "tags-split",
  target: "./src/api/generated/index.ts",
  schemas: "./src/api/generated/model",
}

次のようなものを生成できます:

src/api/generated/
├── users/
│   └── users.ts
├── pois/
│   └── pois.ts
├── itineraries/
│   └── itineraries.ts
└── model/
    ├── userProfile.ts
    ├── poi.ts
    └── itinerary.ts

実際のディレクトリ名とファイル名は、設定と Schema Tags によって決まります。

多数のインターフェイスを持つプロジェクトの場合は、通常、tags-split の方がビジネス モジュールごとにコードを編成するのに適しています。

18. MockとMSW

Orval は OpenAPI Schema から Mock を生成することもできます。

構成:

output: {
  client: "react-query",
  mock: true,
}

Mock は MSW (Mock Service Worker) と連携して HTTP リクエストをインターセプトし、シミュレートされたデータを返すことができます。

基本的なプロセス:

React Component
       ↓
Generated API Function
       ↓
HTTP Request
       ↓
MSW Interceptor
       ↓
Mock Response

以下に適しています:

  • バックエンド インターフェイスはまだ開発されていません。
  • フロントエンドの独立開発。
  • UIテスト。
  • さまざまな API をシミュレートすると結果が返されます。
  • エラーステータスと空のデータをテストします。

注: Mock はインターフェイスの動作をシミュレートすることしかできず、実際のバックエンドが OpenAPI コントラクトと完全に一致していることを証明することはできません。

19. 生成されたコードと手書きのコードの間の責任の境界

これは、Orval を使用する場合の重要なエンジニアリング原則です。

Generated Code

Orval によって自動的に保守されます:

generated/
├── types
├── request functions
├── query hooks
├── mutation hooks
└── query key factories

手動での変更は推奨されません。

Handwritten Code

開発者によって保守されています:

api/
├── axios-instance.ts
├── csrf.ts
├── query-client.ts
├── generated/
└── services/

主に以下を担当します。

  • Axios InstanceとInterceptors。
  • CSRF処理。
  • QueryClient構成。
  • ビジネスレベルのキャッシュ更新。
  • マルチインターフェースの組み合わせ。
  • 特別なエラー処理。

推奨される原則

Orval は標準化されたインターフェイス コードを担当し、ビジネス プロセスとクロスインターフェイス ロジックは開発者によって保守されます。

生成されたファイルに複雑なビジネス ロジックを書き込まないでください。

20. OrvalとReact Hook Form

フォーム送信では、以下を組み合わせることができます。

React Hook Form
       ↓
Form Validation
       ↓
handleSubmit()
       ↓
Orval Mutation Hook
       ↓
Axios
       ↓
Django API

たとえば:

type FormValues = {
  name: string;
};

function ProfileForm() {
  const { register, handleSubmit } = useForm<FormValues>();

  const mutation = useUserProfilePartialUpdate();

  const onSubmit = (values: FormValues) => {
    mutation.mutate({
      data: values,
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />

      <button type="submit" disabled={mutation.isPending}>
        Save
      </button>
    </form>
  );
}

React Hook Form はフォームのステータスと検証を担当し、Orval によって生成された Mutation Hook は API の呼び出しを担当します。

この 2 つは明確な責任を持っており、直接組み合わせて使用​​できます。

21. OrvalとCI/CD

Schema 生成とクライアント コード生成は CI に含めることをお勧めします。

一般的なプロセス:

Django API Changes
         ↓
      pytest
         ↓
drf-spectacular
         ↓
     openapi.yaml
         ↓
       Orval
         ↓
 Generated TypeScript
         ↓
    TypeScript Check
         ↓
      Vite Build

たとえば:

# Django Backend
python manage.py spectacular \
  --file openapi.yaml \
  --validate \
  --fail-on-warn

# React Frontend
pnpm api:generate

# TypeScript
pnpm exec tsc -b

# Build
pnpm build

このようにして、インターフェース契約の不一致の一部が構築段階で発見される可能性があります。

たとえば:

Backend changes field name
           ↓
OpenAPI Schema updated
           ↓
Orval regenerates types
           ↓
Existing frontend code fails typecheck
           ↓
CI detects incompatibility

注: TypeScript チェックは、静的型システムによって検出できる問題のみを検出でき、実行時にバックエンドが Schema に準拠することを保証しません。

22. よくある質問

Q1: Orval は Axios の代わりになりますか?

いいえ。

Orval はコード生成ツール、Axios は HTTP Client です。

Orval は、Axios を使用するリクエスト関数を生成できます。または、他の HTTP Client を構成できます。

Q2: Orval は TanStack Query の代わりになりますか?

いいえ。

Orval は TanStack Query Hooks を生成できます。

ただし、キャッシュ、リクエスト ステータス、再試行、無効化、ガベージ コレクションは TanStack Query によって実装されています。

Q3: Query Key を手動で保守する必要がありますか?

通常は必要ありません。

Orval はインターフェースパラメータに従って Query Key を生成できます。

ただし、複雑なビジネスにおけるキャッシュ構成と Query 間の無効化戦略は、依然として開発者が設計する必要があります。

Q4: Mutation が成功した後、Query は自動的に更新されますか?

デフォルトではそうなると想定することはできません。

onSuccessで使用可能:

queryClient.invalidateQueries(...)

または、Orval のバージョンと構成に従って、生成キャッシュ無効化メカニズムを有効にします。

Q5: 生成されたコードに多くのジェネリックスとオーバーロードが含まれるのはなぜですか?

主に以下をサポートします。

  • カスタム応答タイプ。
  • Query Optionsが違います。
  • initialData 型の推論。
  • 型推論が間違っています。
  • Mutation Variablesタイプ。
  • Axios リクエスト オプション。
  • TanStack Query のタイプ システム。

通常、日常的な使用では、生成されたコード全体を 1 行ずつ理解する必要はありません。

Request Function、Query Key、Query Options、Mutation Options、Hook の関係を理解することに重点が置かれています。

概要

Orval のコア機能は 4 つのレベルに分類できます。

レベルコアコンテンツ
コード生成OpenAPI、operationId、TypeScript Types
HTTP ClientRequest Function、Axios、Mutator
TanStack QueryQuery Key、Query Options、useQuery、useMutation
エンジニアリングの実践キャッシュの無効化、Mock、CI/CD、生成されたコードの管理

最も重要な全体的なアーキテクチャ:

Backend
  Django / DRF
       ↓
  drf-spectacular
       ↓
  OpenAPI Schema
       ↓
      Orval
       ↓
Frontend Generated Code
       │
       ├── Types
       ├── Request Functions
       ├── Query Hooks
       └── Mutation Hooks
                 ↓
          TanStack Query
                 ↓
            React UI

Orval はインターフェイス コントラクトに対応するコードの生成を担当し、Axios は HTTP 通信を担当し、TanStack Query はサーバーの状態管理を担当し、React は UI を担当します。


参考資料