OpenAPI¶
Leverage FastAPI's automatic OpenAPI documentation.
Automatic Generation¶
FastAPI automatically generates OpenAPI schemas from your code:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(
title="My API",
description="A sample API",
version="1.0.0",
)
class Item(BaseModel):
name: str
price: float
@app.get("/items/{item_id}")
async def get_item(item_id: int) -> Item:
"""Get an item by ID."""
pass
Access documentation at:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
Schema Customization¶
App-Level Configuration¶
from fastapi import FastAPI
app = FastAPI(
title="Sartiq API",
description="""
## Overview
The Sartiq API provides programmatic access to photography services.
## Authentication
All endpoints require a Bearer token in the Authorization header.
## Rate Limits
- 100 requests/minute for standard users
- 1000 requests/minute for premium users
""",
version="2.0.0",
terms_of_service="https://example.com/terms",
contact={
"name": "API Support",
"url": "https://example.com/support",
"email": "api@example.com",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
openapi_tags=[
{
"name": "users",
"description": "User management operations",
},
{
"name": "photos",
"description": "Photo upload and management",
},
],
)
Endpoint Documentation¶
from fastapi import FastAPI, Path, Query, Body, HTTPException
@app.post(
"/photos",
response_model=Photo,
status_code=201,
summary="Upload a photo",
description="""
Upload a new photo to the gallery.
The photo will be processed asynchronously and available within 30 seconds.
""",
response_description="The created photo object",
tags=["photos"],
responses={
201: {
"description": "Photo uploaded successfully",
"content": {
"application/json": {
"example": {
"id": 123,
"url": "https://cdn.example.com/photos/123.jpg",
"status": "processing",
}
}
},
},
413: {"description": "Photo too large (max 10MB)"},
415: {"description": "Unsupported media type"},
},
)
async def upload_photo(
file: UploadFile,
title: str = Query(..., description="Photo title", example="Sunset at Beach"),
album_id: int = Query(None, description="Album to add photo to"),
):
"""
Upload a photo with the following constraints:
- **file**: JPEG, PNG, or WebP format
- **max size**: 10MB
- **title**: Required, 1-200 characters
"""
pass
Model Documentation¶
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional, List
class PhotoBase(BaseModel):
"""Base photo schema with common fields."""
title: str = Field(
...,
min_length=1,
max_length=200,
description="Photo title",
example="Sunset at Beach",
)
description: Optional[str] = Field(
None,
max_length=2000,
description="Detailed photo description",
example="A beautiful sunset captured at Malibu Beach",
)
tags: List[str] = Field(
default_factory=list,
max_items=20,
description="Tags for categorization",
example=["sunset", "beach", "nature"],
)
class PhotoCreate(PhotoBase):
"""Schema for creating a new photo."""
pass
class Photo(PhotoBase):
"""Complete photo schema with all fields."""
id: int = Field(..., description="Unique photo identifier")
url: str = Field(..., description="CDN URL for the photo")
thumbnail_url: str = Field(..., description="Thumbnail URL")
width: int = Field(..., description="Width in pixels")
height: int = Field(..., description="Height in pixels")
created_at: datetime = Field(..., description="Upload timestamp")
author_id: int = Field(..., description="ID of the uploader")
model_config = {
"json_schema_extra": {
"example": {
"id": 123,
"title": "Sunset at Beach",
"description": "A beautiful sunset",
"tags": ["sunset", "beach"],
"url": "https://cdn.example.com/photos/123.jpg",
"thumbnail_url": "https://cdn.example.com/photos/123_thumb.jpg",
"width": 1920,
"height": 1080,
"created_at": "2024-01-15T10:30:00Z",
"author_id": 456,
}
}
}
Parameter Documentation¶
from fastapi import Path, Query, Header, Cookie
@app.get("/users/{user_id}/photos")
async def get_user_photos(
user_id: int = Path(
...,
title="User ID",
description="The ID of the user whose photos to retrieve",
example=123,
ge=1,
),
page: int = Query(
1,
title="Page number",
description="Page number for pagination",
ge=1,
example=1,
),
per_page: int = Query(
20,
title="Items per page",
description="Number of items per page",
ge=1,
le=100,
example=20,
),
sort: str = Query(
"-created_at",
title="Sort field",
description="Field to sort by. Prefix with '-' for descending.",
regex="^-?(created_at|title|views)$",
example="-created_at",
),
x_request_id: str = Header(
None,
alias="X-Request-ID",
description="Request tracking ID",
),
):
pass
Response Models¶
Multiple Response Types¶
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
class ErrorResponse(BaseModel):
error: str
message: str
details: Optional[dict] = None
@app.get(
"/photos/{photo_id}",
response_model=Photo,
responses={
404: {
"model": ErrorResponse,
"description": "Photo not found",
"content": {
"application/json": {
"example": {
"error": "not_found",
"message": "Photo with ID 123 not found",
}
}
},
},
403: {
"model": ErrorResponse,
"description": "Access denied",
},
},
)
async def get_photo(photo_id: int):
photo = await db.get(Photo, photo_id)
if not photo:
raise HTTPException(404, detail="Photo not found")
return photo
Union Response Types¶
from typing import Union
class SuccessResponse(BaseModel):
success: bool = True
data: Photo
class ErrorResponse(BaseModel):
success: bool = False
error: str
@app.post("/photos", response_model=Union[SuccessResponse, ErrorResponse])
async def create_photo(photo: PhotoCreate):
try:
result = await save_photo(photo)
return SuccessResponse(data=result)
except ValidationError as e:
return ErrorResponse(error=str(e))
Security Schemes¶
Bearer Token¶
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
app = FastAPI()
# This adds the security scheme to OpenAPI
@app.get("/protected", dependencies=[Depends(security)])
async def protected_route(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
"""
This endpoint requires a Bearer token.
Obtain a token from `/auth/login`.
"""
pass
OAuth2¶
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="auth/token",
scopes={
"read:photos": "Read photos",
"write:photos": "Create and edit photos",
"delete:photos": "Delete photos",
},
)
@app.post("/auth/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""
OAuth2 token endpoint.
Use username and password to obtain an access token.
"""
pass
@app.get("/photos", dependencies=[Depends(oauth2_scheme)])
async def list_photos():
"""Requires `read:photos` scope."""
pass
API Key¶
from fastapi.security import APIKeyHeader, APIKeyQuery
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
async def get_api_key(
header_key: str = Depends(api_key_header),
query_key: str = Depends(api_key_query),
):
api_key = header_key or query_key
if not api_key or not await verify_api_key(api_key):
raise HTTPException(401, "Invalid API key")
return api_key
Grouping with Tags¶
from fastapi import APIRouter
# Create routers with tags
users_router = APIRouter(prefix="/users", tags=["users"])
photos_router = APIRouter(prefix="/photos", tags=["photos"])
admin_router = APIRouter(prefix="/admin", tags=["admin"])
@users_router.get("/")
async def list_users():
"""List all users."""
pass
@photos_router.get("/")
async def list_photos():
"""List all photos."""
pass
@admin_router.get("/stats")
async def get_stats():
"""Get system statistics (admin only)."""
pass
# Include routers
app.include_router(users_router)
app.include_router(photos_router)
app.include_router(admin_router)
Customizing OpenAPI Output¶
Hide Endpoints¶
@app.get("/internal/health", include_in_schema=False)
async def health_check():
"""Not shown in docs."""
return {"status": "ok"}
Custom OpenAPI Schema¶
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="My API",
version="1.0.0",
description="Custom description",
routes=app.routes,
)
# Add custom server
openapi_schema["servers"] = [
{"url": "https://api.example.com", "description": "Production"},
{"url": "https://staging-api.example.com", "description": "Staging"},
]
# Add custom info
openapi_schema["info"]["x-logo"] = {
"url": "https://example.com/logo.png"
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
Exporting OpenAPI Schema¶
# Export to JSON
python -c "from app.main import app; import json; print(json.dumps(app.openapi()))" > openapi.json
# Export to YAML
pip install pyyaml
python -c "
from app.main import app
import yaml
print(yaml.dump(app.openapi(), sort_keys=False))
" > openapi.yaml
Best Practices¶
| Practice | Implementation |
|---|---|
| Use descriptive summaries | Short, action-oriented titles |
| Document all parameters | Description, example, constraints |
| Show example responses | Realistic examples in schema |
| Group with tags | Logical grouping of endpoints |
| Include error responses | Document all possible errors |
| Use consistent models | Reuse schemas across endpoints |
See Also¶
- API Documentation -- Writing effective endpoint and model documentation
- Pydantic Schema Standards -- Schema hierarchy and reusable components that drive OpenAPI generation