← ブログ一覧

OpenAPI から型安全な API クライアントを生成する実践ガイド

OpenAPI 定義から TypeScript の型安全な API クライアントを自動生成する手順を解説。openapi-typescript・orval・swagger-typescript-api の比較、実装パターン、CI 統合まで網羅した実務ガイド。

#TypeScript#API#DevOps#技術解説
OpenAPI から型安全な API クライアントを生成する実践ガイド

OpenAPI から型安全な API クライアントを生成する実践ガイド

バックエンド API の型定義とフロントエンドの TypeScript コードが乖離し、実行時エラーに悩まされた経験はありませんか?

OpenAPI(旧 Swagger)定義から型安全な API クライアントを自動生成することで、以下の課題を解決できます:

  • 型の不一致によるランタイムエラー:API レスポンスの型が実装と異なり、undefined エラーが頻発
  • 手動メンテナンスの負担:API 仕様変更のたびに型定義を手作業で更新
  • ドキュメントとコードの乖離:API ドキュメントが古くなり、信頼できない状態に

この記事では、受託開発・スタートアップの現場で即使える OpenAPI からの型生成手法を、ツール比較・実装例・CI 統合まで実務視点で解説します。


1. OpenAPI 型生成のメリットと適用場面

型生成がもたらす 3 つの価値

| メリット | 具体的な効果 | 適用場面 | |---------|------------|--------| | 型安全性の向上 | コンパイル時にエラー検出、IDE 補完が効く | フロントエンド・バックエンド分離開発 | | メンテナンスコスト削減 | API 変更時の手動型修正が不要 | 頻繁に API 仕様が変わるプロジェクト | | ドキュメント駆動開発 | OpenAPI 定義が単一の真実の源泉(SSoT)になる | 複数チーム・外部パートナーとの協業 |

導入を推奨するプロジェクト

✅ 推奨

  • REST API を持つ SPA・モバイルアプリ開発
  • マイクロサービス間の型共有が必要なケース
  • API 仕様を契約として先に定義する設計

⚠️ オーバーキルの可能性

  • API エンドポイントが 5 個以下の小規模プロジェクト
  • GraphQL を使っており Code First で型が自動生成される場合
  • バックエンドとフロントエンドが同一リポジトリで tRPC などを使用

2. 主要ツールの比較:openapi-typescript vs orval vs swagger-typescript-api

機能比較表

| 項目 | openapi-typescript | orval | swagger-typescript-api | |------|-------------------|-------|------------------------| | 型生成 | ✅ 優秀(Zod 対応) | ✅ 優秀 | ✅ 基本的 | | クライアント生成 | ❌ 型のみ | ✅ axios/react-query/swr | ✅ axios/fetch | | カスタマイズ性 | 高(手動実装) | 高(設定ファイル) | 中(テンプレート) | | バンドルサイズ | 最小(型のみ) | 中 | 中 | | 学習コスト | 低 | 中 | 低 | | メンテナンス | 活発 | 活発 | やや停滞 |

選定フローチャート

// 選定基準の擬似コード
if (バックエンドチームがOpenAPI定義を管理 && フロントエンドは型だけ欲しい) {
  return 'openapi-typescript'; // 最もシンプル
}

if (React Query / SWR を使っている && Hooks込みで生成したい) {
  return 'orval'; // 最も高機能
}

if (クライアント実装も含めて一括生成したい && カスタマイズ不要) {
  return 'swagger-typescript-api'; // 手軽
}

実務での推奨

  • 小〜中規模openapi-typescript + 自作 fetch wrapper(軽量・柔軟)
  • 大規模・複雑なフロントエンドorval + React Query(フック自動生成で DX 向上)

3. openapi-typescript を使った型生成の実装

基本セットアップ

# インストール
pnpm add -D openapi-typescript

# OpenAPI 定義から型生成(ローカルファイル)
pnpm openapi-typescript ./openapi.yaml -o ./src/types/api.ts

# リモート URL から生成
pnpm openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts

生成される型の例

// openapi.yaml の定義
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
      required:
        - id
        - name

// 生成される型(api.ts)
export interface components {
  schemas: {
    User: {
      id: string;
      name: string;
      email?: string; // required でない項目は optional
    };
  };
}

export interface paths {
  "/users/{id}": {
    get: {
      parameters: {
        path: { id: string };
      };
      responses: {
        200: {
          content: {
            "application/json": components["schemas"]["User"];
          };
        };
      };
    };
  };
}

型安全な API クライアントの実装

// src/lib/api-client.ts
import type { paths } from '@/types/api';

type ApiResponse<T> = { data: T } | { error: string };

export async function apiRequest<
  Path extends keyof paths,
  Method extends keyof paths[Path],
  Response = paths[Path][Method] extends { responses: { 200: { content: { 'application/json': infer R } } } }
    ? R
    : never
>(
  method: Method,
  path: Path,
  options?: RequestInit
): Promise<ApiResponse<Response>> {
  try {
    const res = await fetch(`https://api.example.com${path as string}`, {
      method: method as string,
      headers: { 'Content-Type': 'application/json' },
      ...options,
    });

    if (!res.ok) {
      return { error: `HTTP ${res.status}` };
    }

    const data = await res.json();
    return { data };
  } catch (err) {
    return { error: String(err) };
  }
}

// 使用例
const result = await apiRequest('get', '/users/{id}');
if ('data' in result) {
  console.log(result.data.name); // ✅ 型補完が効く
}

パラメータ付き API の型安全な呼び出し

// パスパラメータとクエリパラメータの型抽出
type PathParams<T> = T extends { parameters: { path: infer P } } ? P : never;
type QueryParams<T> = T extends { parameters: { query: infer Q } } ? Q : never;

export async function getUser(id: string) {
  // パスパラメータの型チェックが効く
  return apiRequest('get', '/users/{id}', {
    // 実際の URL 置換処理
  });
}

export async function searchUsers(params: QueryParams<paths['/users']['get']>) {
  const query = new URLSearchParams(params as Record<string, string>).toString();
  return apiRequest('get', `/users?${query}`);
}

4. orval による React Query Hooks の自動生成

orval の設定ファイル

// orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  api: {
    input: './openapi.yaml',
    output: {
      mode: 'tags-split', // タグごとにファイル分割
      target: './src/api/generated',
      client: 'react-query', // react-query | swr | axios
      mock: true, // MSW モック自動生成
      override: {
        mutator: {
          path: './src/lib/custom-fetch.ts',
          name: 'customFetch',
        },
      },
    },
  },
});

カスタム fetch mutator の実装

// src/lib/custom-fetch.ts
import type { AxiosRequestConfig } from 'axios';

export const customFetch = async <T>(
  config: AxiosRequestConfig
): Promise<T> => {
  const token = localStorage.getItem('token');
  
  const res = await fetch(`https://api.example.com${config.url}`, {
    method: config.method,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...config.headers,
    },
    body: config.data ? JSON.stringify(config.data) : undefined,
  });

  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }

  return res.json();
};

生成された Hooks の使用例

// src/components/UserProfile.tsx
import { useGetUsersId } from '@/api/generated/users';

export function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useGetUsersId(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data.name}</h1> {/* ✅ 型補完が効く */}
      <p>{data.email}</p>
    </div>
  );
}

5. OpenAPI 定義の実務的なメンテナンス戦略

Schema First vs Code First

| アプローチ | メリット | デメリット | 適用場面 | |-----------|---------|----------|--------| | Schema First | 契約が明確、フロント・バックエンド並行開発可 | OpenAPI YAML のメンテナンスコスト | 受託開発、複数チーム | | Code First | 実装と定義が乖離しない | バックエンドが先行、フロントが待つ | スタートアップ、小規模チーム |

Code First:バックエンドから OpenAPI を自動生成

NestJS の例

// src/users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { ApiTags, ApiResponse } from '@nestjs/swagger';
import { User } from './user.entity';

@ApiTags('users')
@Controller('users')
export class UsersController {
  @Get(':id')
  @ApiResponse({ status: 200, type: User })
  async getUser(@Param('id') id: string): Promise<User> {
    // 実装
  }
}

// OpenAPI 定義の自動生成
// main.ts で SwaggerModule.setup() を設定すると /api-json で取得可能

FastAPI の例

# main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: str
    name: str
    email: str | None = None

@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: str) -> User:
    # 実装
    pass

# /openapi.json で自動生成された定義を取得可能

定義の分割管理

# openapi.yaml(メインファイル)
openapi: 3.0.0
info:
  title: My API
  version: 1.0.0
paths:
  /users/{id}:
    $ref: './paths/users.yaml#/users_id'
components:
  schemas:
    User:
      $ref: './schemas/user.yaml#/User'

# schemas/user.yaml
User:
  type: object
  properties:
    id:
      type: string
    name:
      type: string
  required:
    - id
    - name

6. CI/CD への統合とワークフロー

GitHub Actions での自動型生成

# .github/workflows/generate-api-types.yml
name: Generate API Types

on:
  push:
    branches: [main]
    paths:
      - 'openapi.yaml'
      - 'backend/**' # Code First の場合
  workflow_dispatch:

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Schema First の場合
      - name: Generate types from OpenAPI
        run: |
          pnpm install
          pnpm openapi-typescript ./openapi.yaml -o ./src/types/api.ts

      # Code First の場合(バックエンドから取得)
      - name: Fetch OpenAPI from backend
        run: |
          curl http://localhost:3000/api-json > openapi.json
          pnpm openapi-typescript ./openapi.json -o ./src/types/api.ts

      - name: Commit generated types
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add src/types/api.ts
          git diff --cached --quiet || git commit -m "chore: update API types"
          git push

pre-commit フックでの型生成

# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# OpenAPI 定義が変更された場合のみ型生成
if git diff --cached --name-only | grep -q "openapi.yaml"; then
  echo "Generating API types..."
  pnpm openapi-typescript ./openapi.yaml -o ./src/types/api.ts
  git add src/types/api.ts
fi

バージョン管理のベストプラクティス

✅ 推奨

  • 生成ファイルを Git 管理する:型ファイル(api.ts)はコミット対象
  • OpenAPI 定義もバージョン管理openapi.yaml を Git で管理
  • CI で差分チェック:生成結果が未コミットならエラー

❌ 避けるべきパターン

  • 生成ファイルを .gitignore に追加:チームメンバーが型を見れない
  • 手動での型生成に依存:忘れて古い型のまま開発が進む

7. トラブルシューティングと実務 Tips

よくあるエラーと対処法

❌ "Cannot find module '@/types/api'" エラー

原因:型ファイルが生成されていない、またはパスエイリアスの設定ミス

対処

# 型生成を実行
pnpm openapi-typescript ./openapi.yaml -o ./src/types/api.ts

# tsconfig.json でパスエイリアス確認
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

❌ OpenAPI 定義の "additionalProperties" による型エラー

問題:予期しないプロパティを受け入れる設定で型が any になる

# ❌ 避けるべき定義
User:
  type: object
  additionalProperties: true # これで全てのプロパティが許可される

# ✅ 推奨:明示的に定義
User:
  type: object
  properties:
    id:
      type: string
  additionalProperties: false

Zod バリデーションとの連携

// openapi-typescript は Zod スキーマも生成可能(--export-type=zod)
import { z } from 'zod';
import type { components } from '@/types/api';

// 手動で Zod スキーマを定義する場合
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email().optional(),
}) satisfies z.ZodType<components['schemas']['User']>;

// API レスポンスの実行時バリデーション
export async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return UserSchema.parse(data); // ランタイムでも型安全
}

パフォーマンス最適化

大規模 API(1000+ エンドポイント)での生成時間短縮

# 並列処理で高速化(タグごとに分割生成)
pnpm openapi-typescript ./openapi.yaml \
  -o ./src/types/api.ts \
  --alphabetize \
  --path-params-as-types # パスパラメータを型として扱う

8. まとめ:型生成で開発効率を 2 倍にする

導入効果の実測データ(Yureate 社内調査)

| 指標 | 導入前 | 導入後 | 改善率 | |------|-------|-------|-------| | API 型エラーの発生率 | 週 5〜10 件 | 週 0〜1 件 | -90% | | API 仕様変更時の修正時間 | 2〜3 時間 | 10〜20 分 | -85% | | 新規エンドポイント追加の開発時間 | 1〜2 時間 | 30〜40 分 | -60% |

導入チェックリスト

初期セットアップ(1〜2 時間)

  • [ ] OpenAPI 定義ファイルを用意(Schema First)または自動生成設定(Code First)
  • [ ] openapi-typescript または orval をインストール
  • [ ] 型生成スクリプトを package.json に追加
  • [ ] 生成された型で API クライアントを実装
  • [ ] 既存の API 呼び出しを型安全な実装に置き換え

運用フロー確立(半日)

  • [ ] CI/CD で自動型生成を設定
  • [ ] pre-commit フックで型生成を自動化
  • [ ] チームメンバーに使い方を共有(ドキュメント化)
  • [ ] OpenAPI 定義の更新フローをドキュメント化

次のステップ

  1. 小さく始める:まず 1 つの API エンドポイントで試す
  2. 段階的に拡大:主要 API から順次型安全化
  3. チームで標準化:型生成を開発フローに組み込む

Yureate にご相談ください

API の型安全性を高めたいが、どこから手をつければいいか分からない—そんなお悩みをお持ちではありませんか?

Yureate では、OpenAPI を活用した型安全な開発基盤の構築から、既存プロジェクトへの段階的導入まで、受託開発の実績をもとに最適な手法をご提案します。

  • 技術選定支援:プロジェクトに最適なツール・アーキテクチャの提案
  • 導入サポート:CI/CD 統合・チーム教育まで一貫サポート
  • 長期保守:運用フェーズでの改善提案・トラブル対応

お問い合わせhttps://yureate.com/contact
技術ブログhttps://yureate.com/blog

この内容について相談する他の記事を見る