Claude generates OpenAPI specs from existing code in minutes
← Back
April 4, 2026Claude6 min read

Claude generates OpenAPI specs from existing code in minutes

Published April 4, 20266 min read

Every project I inherit has the same problem: 30-50 API endpoints with no documentation. The code is the source of truth but reading it to understand the API shape is painful. I discovered that Claude can read route handlers and generate complete, accurate OpenAPI 3.1 specs — request schemas, response shapes, error codes, everything. What used to take a week now takes an afternoon.

The approach

For each route file, I feed Claude the handler code and ask it to produce an OpenAPI path object. Then I combine the individual path objects into a complete spec. This works better than asking Claude to process the entire codebase at once because each route gets focused attention.

The extraction script

python
#!/usr/bin/env python3
# generate-openapi.py

import anthropic
import json
import os
from pathlib import Path

client = anthropic.Anthropic()

OPENAPI_SYSTEM = """You are an API documentation expert.
Given a route handler (Express/FastAPI/etc), generate the OpenAPI 3.1 path item object.

Rules:
- Infer request body schema from what the handler reads (req.body fields, Pydantic models)
- Infer path parameters from the route pattern and how they're used
- Infer query parameters from req.query or FastAPI Query() params
- Document all response shapes: 200/201 success, 400 validation errors, 401/403 auth errors
- If the handler can return 404, document it
- Use $ref for common schemas when you see reuse patterns

Return ONLY valid JSON — the path item object for one route.
Do not include the full OpenAPI document, just the path item."""


def generate_path_item(route_code: str, method: str, path: str) -> dict:
    """Generate OpenAPI path item for a single route."""
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1500,
        system=OPENAPI_SYSTEM,
        messages=[{
            "role": "user",
            "content": f"Generate OpenAPI path item for {method.upper()} {path}:

{route_code}"
        }]
    )
    
    text = response.content[0].text.strip()
    # Remove markdown fences if present
    if text.startswith("```"):
        text = text.split("```")[1]
        if text.startswith("json"):
            text = text[4:]
    
    return json.loads(text)


def process_routes_file(file_path: str, base_path: str = "") -> dict:
    """Process a routes file and return path items."""
    content = Path(file_path).read_text()
    
    # Ask Claude to identify all routes in the file first
    routes_response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": f"""List all API routes in this file as JSON array.
Format: [{{"method": "get", "path": "/users/:id", "handler_name": "getUser"}}]
Return only JSON.

{content}"""
        }]
    )
    
    routes_text = routes_response.content[0].text.strip()
    if routes_text.startswith("```"):
        routes_text = routes_text.split("```")[1]
        if routes_text.startswith("json"):
            routes_text = routes_text[4:]
    
    routes = json.loads(routes_text)
    paths = {}
    
    for route in routes:
        path = base_path + route["path"].replace(":id", "{id}")
        method = route["method"].lower()
        
        # Extract just the handler function for focused context
        handler_prompt = f"File: {file_path}

{content}"
        
        path_item = generate_path_item(handler_prompt, method, path)
        
        if path not in paths:
            paths[path] = {}
        paths[path][method] = path_item
    
    return paths


def build_openapi_spec(
    title: str,
    version: str,
    paths: dict,
    server_url: str = "https://api.example.com",
) -> dict:
    return {
        "openapi": "3.1.0",
        "info": {"title": title, "version": version},
        "servers": [{"url": server_url}],
        "paths": paths,
        "components": {
            "securitySchemes": {
                "bearerAuth": {
                    "type": "http",
                    "scheme": "bearer",
                    "bearerFormat": "JWT"
                }
            }
        }
    }

Running it on a real project

python
import glob
import yaml

# Find all route files
route_files = glob.glob("src/routes/**/*.ts", recursive=True)

all_paths = {}

for route_file in route_files:
    print(f"Processing {route_file}...")
    paths = process_routes_file(route_file, base_path="/api/v1")
    all_paths.update(paths)

# Build complete spec
spec = build_openapi_spec(
    title="My API",
    version="1.0.0",
    paths=all_paths,
    server_url="https://api.myapp.com/v1"
)

# Save as YAML (more readable) and JSON
with open("openapi.yaml", "w") as f:
    yaml.dump(spec, f, default_flow_style=False)

with open("openapi.json", "w") as f:
    json.dump(spec, f, indent=2)

print(f"Generated spec with {len(all_paths)} paths")

Validating the output

Always validate the generated spec before publishing:

bash
npm install -g @redocly/cli
redocly lint openapi.yaml

What Claude gets right and what needs review

Consistently accurate: request body schemas (especially when Pydantic or Zod is used), path parameter types, HTTP status codes that are explicitly returned.

Needs human review: authentication requirements (Claude may not know which routes require auth), pagination schemas (varies by convention), rate limiting headers.

My workflow is to run the generator, fix validation errors, then do a human review of auth and pagination. The spec is 80% accurate out of the box — the remaining 20% takes an hour to clean up versus a week to write from scratch.

Share this
← All Posts6 min read