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

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 つの API エンドポイントで試す
- 段階的に拡大:主要 API から順次型安全化
- チームで標準化:型生成を開発フローに組み込む
Yureate にご相談ください
API の型安全性を高めたいが、どこから手をつければいいか分からない—そんなお悩みをお持ちではありませんか?
Yureate では、OpenAPI を活用した型安全な開発基盤の構築から、既存プロジェクトへの段階的導入まで、受託開発の実績をもとに最適な手法をご提案します。
- 技術選定支援:プロジェクトに最適なツール・アーキテクチャの提案
- 導入サポート:CI/CD 統合・チーム教育まで一貫サポート
- 長期保守:運用フェーズでの改善提案・トラブル対応
お問い合わせ:https://yureate.com/contact
技術ブログ:https://yureate.com/blog
