# API Documentation
## Overview
**API documentation specialist covering OpenAPI specs, documentation-as-code, testing docs, SDK generation, and preventing documentation debt.**
**Core principle**: Documentation is a product feature that directly impacts developer adoption - invest in keeping it accurate, tested, and discoverable.
## When to Use This Skill
Use when encountering:
- **OpenAPI/Swagger**: Auto-generating docs, customizing Swagger UI, maintaining specs
- **Documentation testing**: Ensuring examples work, preventing stale docs
- **Versioning**: Managing multi-version docs, deprecation notices
- **Documentation-as-code**: Keeping docs in sync with code changes
- **SDK generation**: Generating client libraries from OpenAPI specs
- **Documentation debt**: Detecting and preventing outdated documentation
- **Metrics**: Tracking documentation usage and effectiveness
- **Community docs**: Managing contributions, improving discoverability
**Do NOT use for**:
- General technical writing (see `muna-technical-writer` skill)
- API design principles (see `rest-api-design`, `graphql-api-design`)
- Authentication implementation (see `api-authentication`)
## OpenAPI Specification Best Practices
### Production-Quality OpenAPI Specs
**Complete FastAPI example**:
```python
from fastapi import FastAPI, Path, Query, Body
from pydantic import BaseModel, Field
from typing import Optional, List
app = FastAPI(
title="Payment Processing API",
description="""
# Payment API
Process payments with PCI-DSS compliance.
## Features
- Multiple payment methods (cards, ACH, digital wallets)
- Fraud detection
- Webhook notifications
- Test mode for development
## Rate Limits
- Standard: 100 requests/minute
- Premium: 1000 requests/minute
## Support
- Documentation: https://docs.example.com
- Status: https://status.example.com
- Support: api-support@example.com
""",
version="2.1.0",
terms_of_service="https://example.com/terms",
contact={
"name": "API Support",
"url": "https://example.com/support",
"email": "api-support@example.com"
},
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
servers=[
{"url": "https://api.example.com", "description": "Production"},
{"url": "https://sandbox-api.example.com", "description": "Sandbox"}
]
)
# Tag organization
tags_metadata = [
{
"name": "payments",
"description": "Payment operations",
"externalDocs": {
"description": "Payment Guide",
"url": "https://docs.example.com/guides/payments"
}
}
]
app = FastAPI(openapi_tags=tags_metadata)
# Rich schema with examples
class PaymentRequest(BaseModel):
amount: float = Field(
...,
gt=0,
le=999999.99,
description="Payment amount in USD",
example=99.99
)
currency: str = Field(
default="USD",
pattern="^[A-Z]{3}$",
description="ISO 4217 currency code",
example="USD"
)
class Config:
schema_extra = {
"examples": [
{
"amount": 149.99,
"currency": "USD",
"payment_method": "card_visa_4242",
"description": "Premium subscription"
},
{
"amount": 29.99,
"currency": "EUR",
"payment_method": "paypal_account",
"description": "Monthly plan"
}
]
}
# Comprehensive error documentation
@app.post(
"/payments",
summary="Create payment",
description="""
Creates a new payment transaction.
## Processing Time
Typically 2-5 seconds for card payments.
## Idempotency
Use `Idempotency-Key` header to prevent duplicates.
## Test Mode
Use test payment methods in sandbox environment.
""",
responses={
201: {"description": "Payment created", "model": PaymentResponse},
400: {
"description": "Invalid request",
"content": {
"application/json": {
"examples": {
"invalid_amount": {
"summary": "Amount validation failed",
"value": {
"error_code": "INVALID_AMOUNT",
"message": "Amount must be between 0.01 and 999999.99"
}
}
}
}
}
},
402: {"description": "Payment declined"},
429: {"description": "Rate limit exceeded"}
},
tags=["payments"]
)
async def create_payment(payment: PaymentRequest):
pass
```
### Custom OpenAPI Generation
**Add security schemes, custom extensions**:
```python
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# Security schemes
openapi_schema["components"]["securitySchemes"] = {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "Get your API key at https://dashboard.example.com/api-keys"
},
"OAuth2": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://auth.example.com/oauth/authorize",
"tokenUrl": "https://auth.example.com/oauth/token",
"scopes": {
"payments:read": "Read payment data",
"payments:write": "Create payments"
}
},
"clientCredentials": {
"tokenUrl": "https://auth.example.com/oauth/token",
"scopes": {
"payments:read": "Read payment data",
"payments:write": "Create payments"
}
}
}
}
}
# Global security requirement
openapi_schema["security"] = [{"ApiKeyAuth": []}]
# Custom extensions for tooling
openapi_schema["x-api-id"] = "payments-api-v2"
openapi_schema["x-audience"] = "external"
openapi_schema["x-ratelimit-default"] = 100
# Add code samples extension (for Swagger UI)
for path_data in openapi_schema["paths"].values():
for operation in path_data.values():
if isinstance(operation, dict) and "operationId" in operation:
operation["x-code-samples"] = [
{
"lang": "curl",
"source": generate_curl_example(operation)
},
{
"lang": "python",
"source": generate_python_example(operation)
}
]
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
```
## Documentation-as-Code
### Keep Docs in Sync with Code
**Anti-pattern**: Docs in separate repo, manually updated, always stale
**Pattern**: Co-locate docs with code, auto-generate from source
**Implementation**:
```python
# Source of truth: Pydantic models
class PaymentRequest(BaseModel):
"""
Payment request model.
Examples:
Basic payment:
```python
payment = PaymentRequest(
amount=99.99,
currency="USD",
payment_method="pm_card_visa"
)
```
"""
amount: float = Field(..., description="Amount in USD")
currency: str = Field(default="USD", description="ISO 4217 currency code")
class Config:
schema_extra = {
"examples": [
{"amount": 99.99, "currency": "USD", "payment_method": "pm_card_visa"}
]
}
# Docs auto-generated from model
# - OpenAPI spec from Field descriptions
# - Examples from schema_extra
# - Code samples from docstring examples
```
**Prevent schema drift**:
```python
import pytest
from fastapi.testclient import TestClient
def test_openapi_schema_matches_committed():
"""Ensure OpenAPI spec is committed and up-to-date"""
client = TestClient(app)
# Get current OpenAPI spec
current_spec = client.get("/openapi.json").json()
# Load committed spec
with open("docs/openapi.json") as f:
committed_spec = json.load(f)
# Fail if specs don't match
assert current_spec == committed_spec, \
"OpenAPI spec has changed. Run 'make update-openapi-spec' and commit"
def test_all_endpoints_have_examples():
"""Ensure all endpoints have request/response examples"""
client = TestClient(app)
spec = client.get("/openapi.json").json()
for path, methods in spec["paths"].items():
for method, details in methods.items():
if method in ["get", "post", "put", "patch", "delete"]:
# Check request body has example
if "requestBody" in details:
assert "examples" in details["requestBody"]["content"]["application/json"], \
f"{method.upper()} {path} missing request examples"
# Check responses have examples
for status_code, response in details.get("responses", {}).items():
if "content" in response and "application/json" in response["content"]:
assert "examples" in response["content"]["application/json"] or \
"example" in response["content"]["application/json"]["schema"], \
f"{method.upper()} {path} response {status_code} missing examples"
```
### Documentation Pre-Commit Hook
```bash
# .git/hooks/pre-commit
#!/bin/bash
# Regenerate OpenAPI spec
python -c "
from app.main import app
import json
with open('docs/openapi.json', 'w') as f:
json.dump(app.openapi(), f, indent=2)
"
# Check if spec changed
git add docs/openapi.json
# Validate spec
npm run validate:openapi
# Run doc tests
pytest tests/test_documentation.py
```
## Documentation Testing
### Ensure Examples Actually Work
**Problem**: Examples in docs become stale, don't work
**Solution**: Test every code example automatically
```python
# Extract examples from OpenAPI spec
import pytest
import requests
from app.main import app
def get_all_examples_from_openapi():
"""Extract all examples from OpenAPI spec"""
spec = app.openapi()
examples = []
for path, methods in spec["paths"].items():
for method, details in methods.items():
if "examples" in details.get("requestBody", {}).get("content", {}).get("application/json", {}):
for example_name, example_data in details["requestBody"]["content"]["application/json"]["examples"].items():
examples.append({
"path": path,
"method": method,
"example_name": example_name,
"data": example_data["value"]
})
return examples
@pytest.mark.parametrize("example", get_all_examples_from_openapi(), ids=lambda e: f"{e['method']}_{e['path']}_{e['example_name']}")
def test_openapi_examples_are_valid(example, client):
"""Test that all OpenAPI examples are valid requests"""
method = example["method"]
path = example["path"]
data = example["data"]
response = client.request(method, path, json=data)
# Examples should either succeed or fail with expected error
assert response.status_code in [200, 201, 400, 401, 402, 403, 404], \
f"Example {example['example_name']} for {method.upper()} {path} returned unexpected status {response.status_code}"
```
**Test markdown code samples**:
```python
import pytest
import re
import tempfile
import subprocess
def extract_code_blocks_from_markdown(markdown_file):
"""Extract code blocks from markdown"""
with open(markdown_file) as f:
content = f.read()
# Find code blocks with language
pattern = r'```(\w+)\n(.*?)```'
return re.findall(pattern, content, re.DOTALL)
def test_python_examples_in_quickstart():
"""Test that Python examples in quickstart.md execute without errors"""
code_blocks = extract_code_blocks_from_markdown("docs/quickstart.md")
for lang, code in code_blocks:
if lang == "python":
# Write code to temp file
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
# Replace placeholders
code = code.replace("sk_test_abc123...", "test_api_key")
code = code.replace("https://api.example.com", "http://localhost:8000")
f.write(code)
f.flush()
# Run code
result = subprocess.run(
["python", f.name],
capture_output=True,
text=True,
timeout=5
)
assert result.returncode == 0, \
f"Python example failed:\n{code}\n\nError:\n{result.stderr}"
```
### Documentation Coverage Metrics
```python
def test_documentation_coverage():
"""Ensure all endpoints are documented"""
from fastapi.openapi.utils import get_openapi
spec = get_openapi(title="Test", version="1.0.0", routes=app.routes)
missing_docs = []
for path, methods in spec["paths"].items():
for method, details in methods.items():
# Check summary
if not details.get("summary"):
missing_docs.append(f"{method.upper()} {path}: Missing summary")
# Check description
if not details.get("description"):
missing_docs.append(f"{method.upper()} {path}: Missing description")
# Check examples
if "requestBody" in details:
content = details["requestBody"].get("content", {}).get("application/json", {})
if "examples" not in content and "example" not in content.get("schema", {}):
missing_docs.append(f"{method.upper()} {path}: Missing request example")
assert not missing_docs, \
f"Documentation incomplete:\n" + "\n".join(missing_docs)
```
## Interactive Documentation
### Swagger UI Customization
**Custom Swagger UI with branding**:
```python
from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.staticfiles import StaticFiles
app = FastAPI(docs_url=None) # Disable default docs
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=f"{app.title} - API Documentation",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
swagger_favicon_url="/static/favicon.png",
swagger_ui_parameters={
"deepLinking": True,
"displayRequestDuration": True,
"filter": True,
"showExtensions": True,
"tryItOutEnabled": True,
"persistAuthorization": True,
"defaultModelsExpandDepth": 1,
"defaultModelExpandDepth": 1
}
)
```
**Add "Try It Out" authentication**:
```python
from fastapi.openapi.docs import get_swagger_ui_html
@app.get("/docs")
async def custom_swagger_ui():
return get_swagger_ui_html(
openapi_url="/openapi.json",
title="API Docs",
init_oauth={
"clientId": "swagger-ui-client",
"appName": "API Documentation",
"usePkceWithAuthorizationCodeGrant": True
}
)
```
### ReDoc Customization
```python
from fastapi.openapi.docs import get_redoc_html
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url="/openapi.json",
title="API Documentation - ReDoc",
redoc_js_url="/static/redoc.standalone.js",
redoc_favicon_url="/static/favicon.png",
with_google_fonts=True
)
```
**ReDoc configuration options**:
```html