Claude generates OpenAPI specs from existing code in minutes
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
#!/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
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:
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.