Claude CodeでOpenAPI仕様からモックサーバーを構築する

Claude CodeでOpenAPI仕様からモックサーバーを構築する | mohablog
目次

はじめに:なぜOpenAPI仕様からモックサーバーを自動生成するのか

フロントエンドとバックエンドを並行開発しているプロジェクトを想像してください。バックエンドの実装がまだ完了していないのに、フロントエンド側は API との連携実装を進める必要がある。こんな場面、結構多いですよね。

そこで活躍するのがモックサーバーです。OpenAPI(Swagger)仕様書から自動的に API の動作を模擬するサーバーを立ち上げれば、実装の進捗に依存せず開発を進められます。

従来は、モックサーバー生成ツールを手動でインストールして設定するのが一般的でした。しかしClaude Code

この記事で分かること: Claude Code を使った OpenAPI 仕様からのモックサーバー自動構築、生成コードのカスタマイズ方法、本番ライクなレスポンス設計、実装時の落とし穴とその対策

環境とバージョン情報

以下の環境を前提にしています:

  • Claude API: claude-3-5-sonnet-20241022(Code Execution 有効)
  • Python: 3.11 以上
  • Flask: 3.0.0
  • openapi-spec-validator: 0.7.1
  • Pydantic: 2.5.0

Claude Code の code execution 機能は、Claude.com の Pro プランで利用できます。API 経由では claude-3-5-sonnet モデルで対応しており、専用の Python 実行環境が提供されます。

OpenAPI仕様書をClaude Codeで解析する

OpenAPI仕様の最小限の例

まず、Claude Code に渡す OpenAPI 仕様書の例を見てみましょう。これは一般的な ToDo API の仕様です。

openapi: 3.0.0
info:
  title: Todo API
  version: 1.0.0
servers:
  - url: http://localhost:5000
    description: Mock server
paths:
  /todos:
    get:
      summary: Get all todos
      operationId: getTodos
      responses:
        '200':
          description: List of todos
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Todo'
    post:
      summary: Create a new todo
      operationId: createTodo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoInput'
      responses:
        '201':
          description: Created todo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
  /todos/{id}:
    get:
      summary: Get a todo by ID
      operationId: getTodoById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Todo object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
        '404':
          description: Todo not found
    put:
      summary: Update a todo
      operationId: updateTodo
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoInput'
      responses:
        '200':
          description: Updated todo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
components:
  schemas:
    Todo:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        completed:
          type: boolean
        created_at:
          type: string
          format: date-time
      required:
        - id
        - title
        - completed
    TodoInput:
      type: object
      properties:
        title:
          type: string
        completed:
          type: boolean
      required:
        - title

Claude Codeへの入力プロンプト

次に、Claude Code に実行させるプロンプトを見てみます。ここがポイントです。ただ「モックサーバーを作ってください」ではなく、仕様を厳密に伝えることが重要です。

以下の OpenAPI 3.0.0 仕様書に基づいて、Python + Flask でモックサーバーを構築してください。

要件:
1. GET /todos では、固定の 5 個の Todo オブジェクトをリストで返す
2. POST /todos では、リクエストボディから title と completed を取得し、新しい ID(現在のカウント+1)を割り当てて返す
3. GET /todos/{id} では、指定された id のオブジェクトを返す。存在しない場合は 404
4. PUT /todos/{id} では、オブジェクトを更新して返す
5. すべてのレスポンスは UTC タイムゾームで ISO 8601 形式の timestamp を含める
6. CORS は有効にし、すべてのオリジンからのリクエストを受け付ける
7. エラーレスポンスは {"error": "message"} 形式で返す
8. in-memory の辞書でデータを保持(永続化不要)

実行後は、localhost:5000 で起動可能な完全な Python スクリプトを出力してください。

Claude Codeの実行と結果

このプロンプトを Claude Code に渡すと、以下のような Python スクリプトが生成されます。実際に試してみたところ、ほぼ一度で動作するコードが得られました。

from flask import Flask, request, jsonify
from flask_cors import CORS
from datetime import datetime, timezone
from typing import List, Dict, Optional

app = Flask(__name__)
CORS(app)

# In-memory data store
todos: Dict[int, Dict] = {}
next_id = 1

def get_current_timestamp() -> str:
    """Get current timestamp in ISO 8601 format (UTC)"""
    return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')

def create_todo(title: str, completed: bool = False) -> Dict:
    """Create a todo object with timestamp"""
    return {
        'id': None,  # Set by caller
        'title': title,
        'completed': completed,
        'created_at': get_current_timestamp()
    }

@app.route('/todos', methods=['GET'])
def get_todos():
    """Get all todos"""
    return jsonify(list(todos.values())), 200

@app.route('/todos', methods=['POST'])
def create_new_todo():
    """Create a new todo"""
    global next_id
    
    try:
        data = request.get_json()
        if not data or 'title' not in data:
            return jsonify({'error': 'Missing required field: title'}), 400
        
        title = data.get('title')
        completed = data.get('completed', False)
        
        todo = create_todo(title, completed)
        todo['id'] = next_id
        todos[next_id] = todo
        next_id += 1
        
        return jsonify(todo), 201
    except Exception as e:
        return jsonify({'error': str(e)}), 400

@app.route('/todos/', methods=['GET'])
def get_todo(todo_id: int):
    """Get a specific todo by ID"""
    if todo_id not in todos:
        return jsonify({'error': 'Todo not found'}), 404
    return jsonify(todos[todo_id]), 200

@app.route('/todos/', methods=['PUT'])
def update_todo(todo_id: int):
    """Update a specific todo"""
    if todo_id not in todos:
        return jsonify({'error': 'Todo not found'}), 404
    
    try:
        data = request.get_json()
        if not data:
            return jsonify({'error': 'Request body required'}), 400
        
        todo = todos[todo_id]
        if 'title' in data:
            todo['title'] = data['title']
        if 'completed' in data:
            todo['completed'] = data['completed']
        
        return jsonify(todo), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 400

if __name__ == '__main__':
    # Initialize with sample data
    for i in range(1, 6):
        todo = create_todo(f'Sample todo {i}', completed=(i % 2 == 0))
        todo['id'] = i
        todos[i] = todo
    
    next_id = len(todos) + 1
    app.run(debug=True, host='localhost', port=5000)

このコードが生成されるまでにハマったポイントがあります。最初のプロンプトでは「モックサーバーを作ってください」とだけ書いていたのですが、返ってきたコードはflask-restxを使った複雑なデコレータベースの実装でした。その後、「シンプルな Flask のみ」と明示したところ、上記のようなシンプルなコードが得られました。Claude Code では、出力形式や使用ライブラリを明示するほど、期待値に近い結果が得られます

生成されたモックサーバーをカスタマイズする

動的なモックデータの生成

初期データが固定では、テストが単調になってしまいます。ここでよくある落とし穴があります。ランダムなデータを毎回生成すると、UI テストが不安定になることがあるんです。

我々が採用した方法は、seed 値に基づいて疑似ランダムデータを生成するという手法です。これなら再現性がありながらもバリエーションが出ます。

import random
from typing import List

def generate_mock_todos(count: int, seed: int = 42) -> Dict[int, Dict]:
    """Generate mock todos with a seed for reproducibility"""
    random.seed(seed)
    todos = {}
    
    titles = [
        'Learn Claude Code',
        'Build OpenAPI mock server',
        'Write API documentation',
        'Test API endpoints',
        'Deploy to production',
        'Monitor API performance',
        'Optimize database queries',
        'Implement caching strategy'
    ]
    
    for i in range(1, count + 1):
        title = random.choice(titles)
        completed = random.choice([True, False])
        created_at = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
        
        todos[i] = {
            'id': i,
            'title': f'{title} #{i}',
            'completed': completed,
            'created_at': created_at
        }
    
    return todos

初期化時に generate_mock_todos(50, seed=12345) と呼び出せば、常に同じデータセットが生成されます。クライアント側のテストで「最初の 10 件は完了していない」といった前提条件を作れるわけです。

遅延やエラーのシミュレーション

本番環境では、時々 API がもたつくことがあります。そのような状況下での UI の動作をテストしたい場合、モックサーバーの応答時間を調整できると便利です。

import time
from functools import wraps

def simulate_network_delay(seconds: float = 0.5):
    """Decorator to add network delay to endpoint"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            # Check if delay is requested via query parameter
            delay = request.args.get('delay', seconds, type=float)
            time.sleep(delay)
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/todos', methods=['GET'])
@simulate_network_delay(seconds=0.3)
def get_todos():
    """Get all todos with simulated delay"""
    return jsonify(list(todos.values())), 200

クライアントから GET /todos?delay=2.0 とリクエストすれば、2 秒の遅延を発生させられます。本番環境の遅延を再現してローディング状態の UI テストを実施できるというわけです。

ステータスコードの動的返却

さらに進めて、エラーレスポンスもシミュレートしたい場面があります。

@app.route('/todos/', methods=['GET'])
def get_todo(todo_id: int):
    """Get a specific todo by ID"""
    # Check if error simulation is requested
    error_code = request.args.get('error_code', type=int)
    if error_code:
        return jsonify({'error': f'Simulated error {error_code}'}), error_code
    
    if todo_id not in todos:
        return jsonify({'error': 'Todo not found'}), 404
    return jsonify(todos[todo_id]), 200

これで GET /todos/999?error_code=500 とリクエストすれば、500 エラーが返されます。クライアント側のエラーハンドリングを確実にテストできるわけです。

モックサーバーの運用:よくある失敗と対策

問題1:OpenAPI仕様とモックサーバーの乖離

自動生成されたコードも時間とともに修正が加わり、いつの間にか OpenAPI 仕様と異なる動作をするようになる…という現象に直面しました。これは結構やっかいです。

対策としては、定期的に OpenAPI 仕様の検証を実行することです。

from openapi_spec_validator import openapi_v30_spec_validator
import yaml

def validate_openapi_spec(spec_path: str) -> bool:
    """Validate OpenAPI specification"""
    try:
        with open(spec_path, 'r') as f:
            spec = yaml.safe_load(f)
        
        openapi_v30_spec_validator.validate(spec)
        print('✓ OpenAPI spec is valid')
        return True
    except Exception as e:
        print(f'✗ OpenAPI spec validation failed: {e}')
        return False

# Use in test or CI/CD pipeline
if __name__ == '__main__':
    validate_openapi_spec('openapi.yaml')
    # Then run the mock server

CI/CD パイプラインに組み込んで、デプロイ前に必ず検証するようにしましょう。

問題2:複雑なスキーマの取り扱い

OpenAPI 仕様で allOfoneOf といった複合スキーマを使うと、Claude Code の生成コードが対応しきれないことがあります。

スキーマパターン Claude Code の対応度 対策
$ref 基本的に問題なし
allOf 事前に oneOf で統合するか、Claude に明示指示
oneOf 複雑な場合は手動実装推奨
discriminator × 手動実装が必須

この場合の対策は、OpenAPI 仕様を可能な限りシンプルに保つことです。複雑な型定義は、API レスポンスレベルで統合してしまうほうが、実装も検証も楽になります。

問題3:非同期処理への対応不足

大規模なクライアント開発では、モックサーバーも同時に複数のリクエストを処理できる必要があります。Flask の開発サーバーはシングルスレッドなので、一つのリクエストが長くかかると他のリクエストがブロックされてしまいます。

本番に近づいたら、WSGI サーバー(Gunicorn など)でデプロイすることをお勧めします。

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 app:app

-w 4 オプションで 4 つのワーカープロセスを起動しています。これなら複数のリクエストを並行処理できます。

Claude Codeのコード生成の精度を上げるコツ

プロンプトエンジニアリング:具体例の提示

Claude Code に単なる要件を書き連ねるより、期待するコード例の一部を示すほうが、生成結果の精度が上がります。

例えば、こんな感じです:

レスポンス形式の例:
{
  "id": 1,
  "title": "Buy milk",
  "completed": false,
  "created_at": "2024-12-15T10:30:45Z"
}

このフォーマットに統一してください。

こうすることで、生成コードのレスポンス形式が揺らぎません。

段階的なリクエスト:まずシンプルから

一度に複雑な要件を盛り込まず、段階的に進めるのも有効です。

  1. 基本的なエンドポイント(GET, POST)の実装
  2. 詳細エンドポイント(GET by ID, PUT)の追加
  3. エラーハンドリングの強化
  4. データ永続化やログ機能の追加

こうしても生成コードが積み重なっていくので、完成までの時間はほぼ変わりません。むしろ、各段階で確認しながら進められるので、修正が少なくなります。

OpenAPI仕様が先か、モックサーバーが先か

「鶏と卵」みたいな問題ですが、我々のチームではOpenAPI 仕様を先に定義するアプローチを採用しています。理由は:

  • API の契約が明確になり、フロントとバックの認識がズレない
  • Claude Code の生成精度が上がる(曖昧さが減る)
  • 自動テストコード生成の際も、仕様があるほうが楽

実際のプロジェクト:統合テストでの活用

E2Eテストフレームワークとの連携

モックサーバーは、E2E テストのための環境として使うと非常に有用です。

# conftest.py - Pytest fixture
import subprocess
import time
import requests
from pytest import fixture

@fixture(scope='session')
def mock_server():
    """Start mock server for E2E tests"""
    proc = subprocess.Popen(
        ['python', 'mock_server.py'],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    # Wait for server to be ready
    time.sleep(2)
    for _ in range(10):
        try:
            response = requests.get('http://localhost:5000/todos')
            if response.status_code == 200:
                break
        except requests.exceptions.ConnectionError:
            time.sleep(0.5)
    
    yield proc
    
    # Teardown
    proc.terminate()
    proc.wait()

テスト開始時にモックサーバーを立ち上げ、テスト終了後に停止させるという流れです。これなら CI/CD パイプラインでも安定して E2E テストを実行できます。

複数のシナリオ検証

Claude Code で複数のモックサーバーパターンを生成することもできます。

以下の 3 つのシナリオに対応したモックサーバーを生成してください:

シナリオ1「Happy Path」
- すべてのエンドポイントが正常に動作

シナリオ2「Network Degradation」
- GET /todos は 2 秒の遅延を発生させる
- POST /todos は時々 500 エラーを返す(確率 30%)

シナリオ3「Database Unavailable」
- すべてのエンドポイントが 503 Service Unavailable を返す

環境変数 MOCK_SCENARIO で切り替え可能なコードを生成してください。

こうすれば、フロントエンドチームが「通常系」「部分的障害」「完全障害」の 3 パターンで検証できます。

まとめ

  • Claude Code は OpenAPI 仕様から実用的なモックサーバーを自動生成できる。詳細で明確なプロンプトほど、生成精度が高い
  • 生成されたコードはそのまま使えることが多いが、複雑なスキーマや非同期処理は手動調整が必要な場合がある
  • 遅延やエラーのシミュレーション機能を追加すると、クライアント側の検証がより堅牢になる
  • OpenAPI 仕様の定期的な検証と、生成コードの乖離チェックが長期運用の鍵
  • 本番段階では Gunicorn などの WSGI サーバーで起動し、複数リクエストの並行処理に対応させる
  • E2E テストやシナリオベース検証に組み込むことで、モックサーバーの価値が最大化される

よくある質問(FAQ)

Q1:Claude Code で生成したコードは本番利用できますか?

モックサーバーの用途(開発・テスト用)であれば、ほぼそのまま使えます。ただし、本番 API サーバーの代替として使う場合は、セキュリティ(認証・認可)やレート制限、ログ管理などの強化が必要です。その場合は、生成コードをベースに、セキュリティレイヤーを追加してください。

Q2:OpenAPI 2.0(Swagger)にも対応していますか?

Claude Code 自体に制限はありませんが、プロンプトで「OpenAPI 3.0.0 ベースで」と明示することをお勧めします。OpenAPI 2.0 は仕様が古く、生成コードが古いライブラリに依存する可能性があります。可能であれば、OpenAPI 3.0.0 へのマイグレーションを検討してください。

Q3:データベースを連携させることはできますか?

できます。Claude Code に「SQLAlchemy を使用して、SQLite(または PostgreSQL)に接続するコードを生成してください」と指示すれば、ORM ベースの実装が得られます。ただし、初期化スクリプト(マイグレーション)も併せて生成してもらうほうが、セットアップが楽です。

Q4:複数の OpenAPI ファイルをマージしてモックサーバーを作れますか?

プロンプトで「複数の OpenAPI ファイルをマージして一つのサーバーを実装する」と指示すれば、Claude Code がそれに対応したコードを生成します。ただし、エンドポイント名の重複やスキーマの矛盾がないか、事前に確認してください。

Q5:モックサーバーの動作ログを取得したいのですが

Python の logging モジュールを使ったログ出力をリクエストプロンプトに含めてください。「すべてのエンドポイントへのリクエスト・レスポンスを JSON 形式でログファイルに記録する」と指示すれば、ログ機能が組み込まれたコードが生成されます。本番環境では、このログを ELK Stack などの集約ログシステムに送信するのが一般的です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次