Files
gh-greyhaven-ai-claude-code…/skills/documentation-architecture/examples/openapi-generation.md
2025-11-29 18:29:15 +08:00

13 KiB

Example: OpenAPI 3.1 Generation from FastAPI Codebase

Complete workflow showing automatic OpenAPI specification generation from a FastAPI codebase with Pydantic v2 models.

Context

Project: E-commerce API (FastAPI + Pydantic v2 + SQLModel) Problem: Manual API documentation was 3 months out of date, causing integration failures for 2 partner teams Goal: Generate comprehensive OpenAPI 3.1 spec automatically from code with multi-language examples

Initial State:

  • 47 API endpoints with no documentation
  • 12 integration issues per week from stale documentation
  • Manual doc updates taking 4+ hours per release
  • Partners blocked waiting for updated contracts

Step 1: Pydantic v2 Models with Rich Schemas

# app/models/orders.py
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime

class OrderItem(BaseModel):
    product_id: str = Field(..., description="Product identifier")
    quantity: int = Field(..., gt=0, description="Quantity to order")
    unit_price: float = Field(..., gt=0, description="Price per unit in USD")

class OrderCreate(BaseModel):
    """Create a new order for the authenticated user."""
    items: List[OrderItem] = Field(..., min_length=1, description="Order line items")
    shipping_address_id: str = Field(..., description="ID of shipping address")
    
    model_config = {
        "json_schema_extra": {
            "examples": [{
                "items": [{"product_id": "prod_123", "quantity": 2, "unit_price": 29.99}],
                "shipping_address_id": "addr_456"
            }]
        }
    }

class Order(BaseModel):
    """Order with calculated totals."""
    id: str
    user_id: str
    items: List[OrderItem]
    subtotal: float = Field(..., description="Sum of all item prices")
    tax: float = Field(..., description="Calculated tax amount")
    total: float = Field(..., description="Final order total")
    status: str = Field(..., description="pending, processing, shipped, delivered, cancelled")
    created_at: datetime

Step 2: Enhanced OpenAPI Generation

# app/main.py
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

app = FastAPI()

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
        
    openapi_schema = get_openapi(
        title="Grey Haven E-Commerce API",
        version="1.0.0",
        description="E-commerce API with JWT auth. Rate limit: 1000 req/hour (authenticated).",
        routes=app.routes,
    )
    
    # Add security schemes
    openapi_schema["components"]["securitySchemes"] = {
        "BearerAuth": {
            "type": "http",
            "scheme": "bearer",
            "bearerFormat": "JWT",
            "description": "JWT token from /auth/login"
        }
    }
    openapi_schema["security"] = [{"BearerAuth": []}]
    
    # Add error response schema
    openapi_schema["components"]["schemas"]["ErrorResponse"] = {
        "type": "object",
        "required": ["error", "message"],
        "properties": {
            "error": {"type": "string", "example": "INSUFFICIENT_STOCK"},
            "message": {"type": "string", "example": "Product has insufficient stock"},
            "details": {"type": "object", "additionalProperties": True}
        }
    }
    
    # Add rate limit headers
    openapi_schema["components"]["headers"] = {
        "X-RateLimit-Limit": {"description": "Request limit per hour", "schema": {"type": "integer"}},
        "X-RateLimit-Remaining": {"description": "Remaining requests", "schema": {"type": "integer"}},
        "X-RateLimit-Reset": {"description": "Reset timestamp", "schema": {"type": "integer"}}
    }
    
    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi

Step 3: FastAPI Route with Complete Documentation

# app/routers/orders.py
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter(prefix="/api/v1/orders", tags=["orders"])

@router.post("/", response_model=Order, status_code=201)
async def create_order(
    order_data: OrderCreate,
    current_user: User = Depends(get_current_user),
    session: Session = Depends(get_session)
) -> Order:
    """
    Create a new order for the authenticated user.
    
    The order will be created in 'pending' status and total calculated
    including applicable taxes based on shipping address.
    
    **Requires**:
    - Valid JWT authentication token
    - At least one item in the order
    - Valid shipping address ID owned by the user
    
    **Returns**: Created order with calculated totals
    
    **Raises**:
    - **401 Unauthorized**: If user is not authenticated
    - **404 Not Found**: If shipping address not found
    - **400 Bad Request**: If product stock insufficient or validation fails
    - **429 Too Many Requests**: If rate limit exceeded
    """
    # Validate shipping address belongs to user
    address = session.get(ShippingAddress, order_data.shipping_address_id)
    if not address or address.user_id != current_user.id:
        raise HTTPException(404, detail="Shipping address not found")
    
    # Check stock availability
    for item in order_data.items:
        product = session.get(Product, item.product_id)
        if not product or product.stock < item.quantity:
            raise HTTPException(
                400,
                detail={
                    "error": "INSUFFICIENT_STOCK",
                    "message": f"Product {item.product_id} has insufficient stock",
                    "details": {
                        "product_id": item.product_id,
                        "requested": item.quantity,
                        "available": product.stock if product else 0
                    }
                }
            )
    
    # Create order and calculate totals
    order = Order(
        user_id=current_user.id,
        items=order_data.items,
        subtotal=sum(item.quantity * item.unit_price for item in order_data.items)
    )
    order.tax = order.subtotal * 0.08  # 8% tax
    order.total = order.subtotal + order.tax
    order.status = "pending"
    
    session.add(order)
    session.commit()
    return order

Step 4: Multi-Language Code Examples

Automated Example Generation

# scripts/generate_examples.py
def generate_examples(openapi_spec):
    """Generate TypeScript, Python, and cURL examples for each endpoint."""
    
    examples = {}
    
    for path, methods in openapi_spec["paths"].items():
        for method, details in methods.items():
            operation_id = details.get("operationId", f"{method}_{path}")
            
            # TypeScript example
            examples[f"{operation_id}_typescript"] = f'''
const response = await fetch('https://api.greyhaven.com{path}', {{
  method: '{method.upper()}',
  headers: {{
    'Authorization': 'Bearer YOUR_API_TOKEN',
    'Content-Type': 'application/json'
  }},
  body: JSON.stringify({{
    items: [{{ product_id: "prod_123", quantity: 2, unit_price: 29.99 }}],
    shipping_address_id: "addr_456"
  }})
}});
const order = await response.json();
'''
            
            # Python example
            examples[f"{operation_id}_python"] = f'''
import requests

response = requests.{method}(
    'https://api.greyhaven.com{path}',
    headers={{'Authorization': 'Bearer YOUR_API_TOKEN'}},
    json={{
        'items': [{{'product_id': 'prod_123', 'quantity': 2, 'unit_price': 29.99}}],
        'shipping_address_id': 'addr_456'
    }}
)
order = response.json()
'''
            
            # cURL example
            examples[f"{operation_id}_curl"] = f'''
curl -X {method.upper()} https://api.greyhaven.com{path} \\
  -H "Authorization: Bearer YOUR_API_TOKEN" \\
  -H "Content-Type: application/json" \\
  -d '{{"items": [{{"product_id": "prod_123", "quantity": 2, "unit_price": 29.99}}], "shipping_address_id": "addr_456"}}'
'''
    
    return examples

Step 5: Interactive Swagger UI

# app/main.py (enhanced)
from fastapi.openapi.docs import get_swagger_ui_html

@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
    return get_swagger_ui_html(
        openapi_url="/openapi.json",
        title=f"{app.title} - API Documentation",
        swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
        swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
        swagger_ui_parameters={
            "persistAuthorization": True,  # Remember auth token
            "displayRequestDuration": True,  # Show request timing
            "filter": True,  # Enable filtering
            "tryItOutEnabled": True  # Enable try-it-out by default
        }
    )

Step 6: CI/CD Auto-Generation

# .github/workflows/generate-docs.yml
name: Generate API Documentation

on:
  push:
    branches: [main]
    paths: ['app/**/*.py']

jobs:
  generate-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      
      - name: Generate OpenAPI spec
        run: |
          pip install -r requirements.txt
          python -c "
          from app.main import app
          import json
          with open('docs/openapi.json', 'w') as f:
              json.dump(app.openapi(), f, indent=2)
          "
      
      - name: Generate code examples
        run: python scripts/generate_examples.py
      
      - name: Validate OpenAPI
        run: npx @redocly/cli lint docs/openapi.json
      
      - name: Deploy to Cloudflare Pages
        run: |
          npm install -g wrangler
          wrangler pages deploy docs/ --project-name=api-docs
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Generated OpenAPI Specification (Excerpt)

openapi: 3.1.0
info:
  title: Grey Haven E-Commerce API
  version: 1.0.0
  description: E-commerce API with JWT auth. Rate limit: 1000 req/hour.

servers:
  - url: https://api.greyhaven.com
    description: Production

paths:
  /api/v1/orders:
    post:
      summary: Create a new order
      description: Create order in 'pending' status with calculated totals
      operationId: createOrder
      tags: [orders]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
      responses:
        '201':
          description: Order created successfully
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          description: Validation error or insufficient stock
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized (invalid token)
        '429':
          description: Rate limit exceeded

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  
  schemas:
    OrderItem:
      type: object
      required: [product_id, quantity, unit_price]
      properties:
        product_id:
          type: string
          example: "prod_123"
        quantity:
          type: integer
          minimum: 1
          example: 2
        unit_price:
          type: number
          minimum: 0.01
          example: 29.99

Results

Before

  • Manual documentation 3 months out of date
  • 47 endpoints with no docs
  • 12 integration issues per week
  • 4+ hours manual doc updates per release
  • Partners blocked waiting for updated contracts

After

  • OpenAPI spec auto-generated on every commit
  • 100% endpoint coverage with examples
  • Interactive Swagger UI with try-it-out
  • Multi-language examples (TypeScript, Python, cURL)
  • Complete error response documentation

Improvements

  • Integration issues: 12/week → 0.5/week (96% reduction)
  • Doc update time: 4 hours → 0 minutes (automated)
  • Partner satisfaction: 45% → 98%
  • Time-to-integration: 2 weeks → 2 days

Partner Feedback

  • "The interactive docs with try-it-out saved us days of testing"
  • "Code examples in our language made integration trivial"
  • "Error responses are fully documented - no guesswork"

Key Lessons

  1. Automation is Critical: Manual docs will always drift from code
  2. Pydantic v2 Schema: Excellent OpenAPI generation with field validators
  3. Multi-Language Examples: Dramatically improved partner integration speed
  4. Interactive Docs: Try-it-out functionality reduced support tickets
  5. CI/CD Integration: Documentation stays current automatically
  6. Error Documentation: Complete error schemas eliminated guesswork

Prevention Measures

Implemented:

  • Auto-generation on every commit (GitHub Actions)
  • OpenAPI spec validation in CI/CD
  • Interactive Swagger UI deployed to Cloudflare Pages
  • Multi-language code examples generated
  • Complete error response schemas
  • Rate limiting documentation

Ongoing:

  • SDK auto-generation from OpenAPI spec (TypeScript, Python clients)
  • Contract testing (validate API matches OpenAPI spec)
  • Changelog generation from git commits

Related: architecture-docs.md | coverage-validation.md | Return to INDEX