なぜAPI設計からコード生成が必要なのか
フリーランスとして複数のプロジェクトに関わっていると、API仕様書とコード実装がズレるという厄介な問題に直面することが多いですね。サーバーサイドがJSON Schemaで定義した型と、クライアント側の実装が微妙に異なっていたり、APIエンドポイントの変更に気づかずクライアントコードが古い仕様で動いていたり。こうした不整合は本番環境でのバグにつながります。
Claude Codeを活用すると、OpenAPI/JSON Schema仕様をベースに、型安全なクライアントコードを自動生成できます。これにより、API仕様が真実の源(Single Source of Truth)となり、スキーマの変更と実装の同期が取れやすくなるんです。
本記事では、Claude CodeでTypeScript/Pythonの型安全なクライアントライブラリを生成する具体的な流れを、実際のプロジェクト例を交えて解説します。バージョンはClaude 3.5 Sonnet、TypeScript 5.3、Python 3.12を想定しています。
API仕様をメタデータとして定義する
OpenAPI仕様の最小限の構成
まず、API仕様をOpenAPI 3.1形式で定義します。公式ドキュメントによると、OpenAPI仕様はサーバーとクライアントの間で契約を結ぶためのメタデータとして機能します。
openapi: 3.1.0
info:
title: E-commerce API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/products:
get:
operationId: listProducts
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
- name: offset
in: query
schema:
type: integer
default: 0
responses:
'200':
description: Product list
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Product'
total:
type: integer
/products/{id}:
get:
operationId: getProduct
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Product details
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
description: Product not found
components:
schemas:
Product:
type: object
required:
- id
- name
- price
properties:
id:
type: string
format: uuid
name:
type: string
price:
type: number
exclusiveMinimum: 0
description:
type: string
nullable: true
createdAt:
type: string
format: date-time
このYAML形式の仕様には、エンドポイント、パラメータ、リクエスト/レスポンスボディの型情報が記述されています。operationIdを指定することで、生成されるコード内での関数名が一貫性を持ちます。
JSON Schemaで複雑な型も型安全に
OpenAPI内のcomponents.schemasで複雑なデータ構造を定義できます。Discriminator(判別子)を使ったポリモーフィズムも表現できるため、API仕様の段階で型の複雑さを表現することが重要ですね。
Discount:
type: object
discriminator:
propertyName: type
required:
- type
- value
properties:
type:
type: string
enum:
- percentage
- fixed
value:
type: number
oneOf:
- $ref: '#/components/schemas/PercentageDiscount'
- $ref: '#/components/schemas/FixedDiscount'
PercentageDiscount:
allOf:
- $ref: '#/components/schemas/Discount'
- type: object
properties:
value:
type: number
minimum: 0
maximum: 100
FixedDiscount:
allOf:
- $ref: '#/components/schemas/Discount'
- type: object
properties:
value:
type: number
exclusiveMinimum: 0
Claude CodeでTypeScript型定義を自動生成する
プロンプト設計がコード生成の品質を左右する
Claude Codeに「OpenAPI仕様からTypeScript型定義を生成する」という指示を与える際、プロンプトの精度が重要になります。調べてみたら、具体的なジェネレーターロジックを明示すると、より実用的なコードが生成されることがわかりました。
以下はClaude CodeでのPrompt例です。
以下のOpenAPI 3.1仕様からTypeScript型定義ファイルを生成してください。
- components.schemasをTypeScript インターフェース/型エイリアスに変換
- required プロパティを必須フィールド、そうでないものはOptionalに
- format: date-timeはDate型に、format: uuidはstring型に
- oneOf/allOfの場合は union型またはintersection型に
- すべての型にJSDocコメントを付与
このプロンプトでClaude Codeが生成する出力例:
/**
* Product representation in the API
*/
export interface Product {
/** Unique identifier (UUID) */
id: string;
/** Product name */
name: string;
/** Price in currency (must be positive) */
price: number;
/** Optional product description */
description?: string | null;
/** Creation timestamp */
createdAt: Date;
}
/**
* Product list response
*/
export interface ProductListResponse {
/** Array of products */
items: Product[];
/** Total number of products */
total: number;
}
/**
* Discount type discriminator
*/
export type Discount = PercentageDiscount | FixedDiscount;
/**
* Discount with percentage reduction
*/
export interface PercentageDiscount {
type: 'percentage';
/** Percentage value (0-100) */
value: number;
}
/**
* Fixed amount discount
*/
export interface FixedDiscount {
type: 'fixed';
/** Fixed discount amount (must be positive) */
value: number;
}
この生成されたインターフェースは、TypeScriptコンパイラで型チェックされるため、ランタイムエラーを事前に防ぐことができます。
APIクライアントクラスの生成
型定義だけでなく、API呼び出しを行うクライアントクラスも生成しましょう。Claude CodeでのPrompt:
上記のOpenAPI仕様から、以下の機能を持つTypeScript APIクライアントクラスを生成してください。
- 各operationIdをメソッド化(例:listProducts、getProduct)
- パラメータは型安全な引数に
- 戻り値は生成した型定義を使用
- エラーハンドリングはHTTPステータスコードに基づいて
- axiosを依存として想定し、リクエストメソッドはasync/await対応
生成コード例:
import axios, { AxiosInstance, AxiosError } from 'axios';
import {
Product,
ProductListResponse,
Discount,
} from './types';
/**
* API Client for E-commerce API
*/
export class ECommerceAPIClient {
private client: AxiosInstance;
constructor(baseURL: string = 'https://api.example.com/v1') {
this.client = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* List all products with pagination
*/
async listProducts(options?: {
limit?: number;
offset?: number;
}): Promise {
try {
const response = await this.client.get('/products', {
params: {
limit: options?.limit ?? 20,
offset: options?.offset ?? 0,
},
});
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get a single product by ID
*/
async getProduct(id: string): Promise {
try {
const response = await this.client.get(`/products/${id}`);
return response.data;
} catch (error) {
if (
axios.isAxiosError(error) &&
error.response?.status === 404
) {
throw new Error(`Product with ID ${id} not found`);
}
throw this.handleError(error);
}
}
private handleError(error: unknown): Error {
if (axios.isAxiosError(error)) {
return new Error(
`API Error: ${error.response?.status} - ${error.response?.statusText}`
);
}
return error instanceof Error ? error : new Error('Unknown error');
}
}
このクライアントクラスは完全に型安全です。メソッドの引数を間違えたり、存在しないプロパティにアクセスしようとしたりすると、TypeScriptコンパイラが即座にエラーを指摘します。
Pythonでの型安全なクライアント生成
Pydanticモデルの自動生成
Pythonではモダンなコード生成に向けて、Pydanticライブラリを使うのが標準的になってきました。Claude CodeでOpenAPI仕様からPydantic v2形式のモデルを生成するプロンプト:
OpenAPI 3.1仕様からPydantic v2のモデルクラスを生成してください。
- components.schemasごとに BaseModel を継承したクラスを生成
- 型ヒントはPython 3.12の標準形式(typing.Optional ではなく | None を使用)
- Docstringはスキーマの description から自動抽出
- format: date-time は datetime.datetime に、format: uuid は uuid.UUID に
- validation でOpenAPI制約(minimum, maximum, pattern等)を Field()で表現
生成結果:
from datetime import datetime
from uuid import UUID
from typing import Optional
from pydantic import BaseModel, Field
class Product(BaseModel):
"""Product representation in the API."""
id: UUID = Field(..., description="Unique identifier (UUID)")
name: str = Field(..., description="Product name")
price: float = Field(..., gt=0, description="Price in currency (must be positive)")
description: Optional[str] = Field(
None, description="Optional product description"
)
created_at: datetime = Field(..., description="Creation timestamp")
class Config:
# Allow population by field name or alias
populate_by_name = True
# JSON encoder config
json_schema_extra = {
"examples": [{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example Product",
"price": 29.99,
"description": "A sample product",
"created_at": "2024-01-15T10:30:00Z",
}]
}
class ProductListResponse(BaseModel):
"""Product list response."""
items: list[Product] = Field(..., description="Array of products")
total: int = Field(..., description="Total number of products")
class Discount(BaseModel):
"""Base discount model with type discrimination."""
type: str = Field(..., description="Discount type")
value: float = Field(..., description="Discount value")
HTTPクライアントの生成(httpx活用)
Pythonではasyncioを前提にhttpxライブラリを使うことが増えていますね。Claude CodeでOpenAPI仕様からPythonのAPIクライアントを生成する際、async/await対応の実装が現場で求められることが多いです。
import httpx
from typing import Optional
from models import Product, ProductListResponse
class ECommerceAPIClient:
"""Async API client for E-commerce API."""
def __init__(
self,
base_url: str = "https://api.example.com/v1",
timeout: float = 10.0,
):
self.base_url = base_url
self.timeout = timeout
self._client: Optional[httpx.AsyncClient] = None
async def __aenter__(self):
"""Context manager entry."""
self._client = httpx.AsyncClient(
base_url=self.base_url,
timeout=self.timeout,
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
if self._client:
await self._client.aclose()
async def list_products(
self,
limit: int = 20,
offset: int = 0,
) -> ProductListResponse:
"""List all products with pagination.
Args:
limit: Maximum number of products to return (default 20)
offset: Number of products to skip (default 0)
Returns:
ProductListResponse with items and total count
Raises:
httpx.HTTPStatusError: If API returns an error status
"""
if not self._client:
raise RuntimeError("Client not initialized. Use async with statement.")
response = await self._client.get(
"/products",
params={"limit": limit, "offset": offset},
)
response.raise_for_status()
return ProductListResponse(**response.json())
async def get_product(self, product_id: str) -> Product:
"""Get a single product by ID.
Args:
product_id: UUID of the product
Returns:
Product details
Raises:
httpx.HTTPStatusError: If product not found or API error
"""
if not self._client:
raise RuntimeError("Client not initialized. Use async with statement.")
try:
response = await self._client.get(f"/products/{product_id}")
response.raise_for_status()
return Product(**response.json())
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise ValueError(f"Product {product_id} not found") from e
raise
生成されたコードをテストする
テストの自動生成(型安全なモック)
Claude Codeで型定義とクライアント実装を生成したら、次はテストコードも自動生成しましょう。Pytestのfixture併用で、本番環境との連携をテストできます。
import pytest
import httpx
from unittest.mock import AsyncMock, patch
from uuid import uuid4
from datetime import datetime
from client import ECommerceAPIClient
from models import Product, ProductListResponse
@pytest.fixture
async def api_client():
"""Fixture for API client."""
async with ECommerceAPIClient(
base_url="https://api.example.com/v1"
) as client:
yield client
@pytest.fixture
def sample_product():
"""Fixture for sample product."""
return Product(
id=uuid4(),
name="Test Product",
price=99.99,
description="A test product",
created_at=datetime.now(),
)
@pytest.mark.asyncio
async def test_list_products(api_client, sample_product):
"""Test listing products."""
mock_response = ProductListResponse(
items=[sample_product],
total=1,
)
with patch.object(
api_client._client,
"get",
new_callable=AsyncMock,
) as mock_get:
mock_get.return_value.json.return_value = mock_response.model_dump(by_alias=True)
mock_get.return_value.raise_for_status.return_value = None
result = await api_client.list_products(limit=10)
assert result.total == 1
assert len(result.items) == 1
assert result.items[0].name == "Test Product"
mock_get.assert_called_once_with(
"/products",
params={"limit": 10, "offset": 0},
)
@pytest.mark.asyncio
async def test_get_product_not_found(api_client):
"""Test getting non-existent product."""
with patch.object(
api_client._client,
"get",
new_callable=AsyncMock,
) as mock_get:
mock_get.return_value.raise_for_status.side_effect = httpx.HTTPStatusError(
"404 Not Found",
request=httpx.Request("GET", "http://test"),
response=httpx.Response(404),
)
with pytest.raises(ValueError, match="not found"):
await api_client.get_product("invalid-id")
このテストコードは、型安全なPydanticモデルを活用してモックレスポンスを作成しているため、スキーマ変更時に自動的に破綻が検出されます。
スキーマ変更時のワークフロー
変更の影響範囲を自動検出
API仕様が変わった時の流れをシンプルにできるのが、このアプローチの大きなメリットです。
- サーバーサイドチームがOpenAPI仕様ファイルを更新
- Claude Codeでクライアント型定義とクライアント実装を再生成
- TypeScriptやPythonのコンパイラ/型チェッカーが、破綻した呼び出しをすぐに指摘
- 開発者は指摘された箇所だけを修正すればOK
これまでのように「APIの仕様書を読み落とす」「パラメータの型を間違える」といったミスが根本的に減ります。
バージョニング戦略
複数のAPI バージョンをサポートする場合、OpenAPI仕様ファイルを api-v1.yaml、api-v2.yaml のように分けて、それぞれClaude Codeで別々のクライアントを生成するのが実用的です。
src/
├── api-clients/
│ ├── v1/
│ │ ├── types.ts
│ │ ├── client.ts
│ │ └── __init__.py (Pythonの場合)
│ └── v2/
│ ├── types.ts
│ ├── client.ts
│ └── __init__.py
└── specs/
├── api-v1.yaml
└── api-v2.yaml
よくあるハマりどころ
null許容フィールドと必須フィールドの混同
OpenAPI仕様でrequired配列に含まれていないフィールドは、自動生成時にOptional/nullable型になります。これを見落とすと、ランタイムでundefinedやNoneが期待せず現れます。Claude Codeが生成する際、JSDocやDocstringに「Optional」と明記されていることを確認してください。
日時フォーマットのタイムゾーン問題
OpenAPI仕様でformat: date-timeと書くと、ISO 8601形式(例:2024-01-15T10:30:00Z)が標準です。Pydanticはデフォルトでこれをdatetime.datetimeに変換しますが、デシリアライズ時にタイムゾーン情報が失われることがあります。型定義にField(..., json_schema_extra={"format": "date-time"})を明記すると、より一貫性が保たれます。
まとめ
- OpenAPI/JSON Schema仕様をメタデータの真実の源にすることで、スキーマ漂流を防ぎます
- Claude Codeで型定義とクライアント実装を自動生成すれば、手書きバグが減り開発速度が上がります
- TypeScriptのaxios/Pythonのhttpxで型安全なAPIクライアントを実装し、ランタイムエラーを事前に検出します
- スキーマ変更時の影響範囲が自動的に型チェッカーで指摘されるため、リグレッション防止に有効です
- テストコードも自動生成されるため、保守性が飛躍的に向上します
よくある質問(FAQ)
既存のAPIクライアントライブラリ(swagger-clientなど)との使い分けは?
swagger-clientはランタイムにスキーマを読み込む動的な手法ですが、Claude Codeの生成アプローチはビルド時の静的コード生成です。型安全性が強い静的生成の方が、TypeScriptやPythonのモダンな開発では好まれる傾向にあります。ただし、非常に動的なAPI(GraphQL等)の場合は、ランタイム生成の方が柔軟かもしれません。
Claude Codeで生成したコードの著作権や再利用は?
Claude Codeが生成したコードはあなたが所有できます。本番プロジェクトでの使用に問題はありません。ただし生成コードにはバグがある可能性があるので、テストと code review は必須です。
大規模なOpenAPI仕様(数百エンドポイント)でも一括生成できる?
Claude Codeの出力トークン制限に引っかかる可能性があります。その場合、エンドポイントを機能ごとに複数のOpenAPI仕様ファイルに分割し、個別に生成することをお勧めします。generated code を monorepo で一元管理するのが実用的なアプローチです。

