API Documentation with OpenAPI
Create comprehensive, interactive API documentation that developers love using OpenAPI/Swagger
Why Documentation Matters
Great APIs have great documentation. The best-designed API is useless if developers can't figure out how to use it. Documentation is the difference between an API that gets adopted and one that gets abandoned.
Consider Stripe's API documentation: comprehensive examples in multiple languages, interactive testing, clear error codes, and beautiful design. This documentation excellence is a key reason Stripe dominates the payment API market.
OpenAPI (formerly Swagger) is the industry-standard specification for describing RESTful APIs. It enables automatic documentation generation, interactive API explorers, client SDK generation, and contract testing. In this lesson, you'll master OpenAPI 3.0/3.1 and learn to create documentation that developers love.
Lesson Sections
OpenAPI Specification Fundamentals
The OpenAPI Specification (OAS) is a language-agnostic standard for describing RESTful APIs. It's written in YAML or JSON and provides a complete description of your API: endpoints, parameters, request/response schemas, authentication, and more.
OpenAPI Document Structure
| Field | Description | Required |
|---|---|---|
openapi | OpenAPI Specification version (e.g., "3.0.3", "3.1.0") | Yes |
info | API metadata (title, version, description, contact, license) | Yes |
servers | Array of server URLs (production, staging, development) | No |
paths | Available endpoints and their operations | Yes |
components | Reusable schemas, parameters, responses, security schemes | No |
security | Global security requirements | No |
tags | Group endpoints by category | No |
Basic OpenAPI Document Example
openapi: 3.1.0
info:
title: Task Management API
version: 1.0.0
description: |
A RESTful API for managing tasks and projects.
## Features
- Create, read, update, delete tasks
- Organize tasks into projects
- Assign tasks to users
- Track task status and priority
contact:
name: API Support
email: support@example.com
url: https://example.com/support
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.example.com/v1
description: Production server
- url: https://staging-api.example.com/v1
description: Staging server
- url: http://localhost:8000/v1
description: Development server
paths:
/tasks:
get:
summary: List all tasks
description: Retrieve a paginated list of tasks
tags:
- Tasks
parameters:
- name: page
in: query
description: Page number for pagination
required: false
schema:
type: integer
default: 1
minimum: 1
- name: limit
in: query
description: Number of items per page
required: false
schema:
type: integer
default: 20
minimum: 1
maximum: 100
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
total:
type: integer
page:
type: integer
post:
summary: Create a new task
description: Create a new task with the provided details
tags:
- Tasks
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TaskCreate'
responses:
'201':
description: Task created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
'400':
description: Invalid input
components:
schemas:
Task:
type: object
required:
- id
- title
- status
properties:
id:
type: integer
description: Unique task identifier
example: 123
title:
type: string
description: Task title
example: "Implement user authentication"
description:
type: string
description: Detailed task description
example: "Add JWT-based authentication to the API"
status:
type: string
enum: [todo, in_progress, done]
description: Current task status
example: "in_progress"
created_at:
type: string
format: date-time
description: Task creation timestamp
TaskCreate:
type: object
required:
- title
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
maxLength: 1000
status:
type: string
enum: [todo, in_progress, done]
default: todoKey Concepts
- $ref: Reference reusable components (DRY principle)
- tags: Group related endpoints for organization in docs
- servers: Allow testing against different environments
- schemas: Define data structures once, reuse everywhere
- format: Specify data formats (date-time, email, uuid, etc.)
OpenAPI 3.0 vs 3.1 Differences
OpenAPI 3.0
- Most widely adopted version
- Custom schema validation
- Uses
nullable: true - Separate
examplesfield
OpenAPI 3.1 (Latest)
- Full JSON Schema 2020-12 support
- Uses
type: [string, null] - Webhooks support
- Better discriminator support
Automatic Documentation in FastAPI
FastAPI automatically generates OpenAPI documentation from your code. Type hints, Pydantic models, and docstrings are used to build comprehensive API specs without manual configuration.
Zero-Configuration Documentation
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Task(BaseModel):
id: int
title: str = Field(..., min_length=1, max_length=200)
description: str | None = Field(None, max_length=1000)
status: str = Field("todo", pattern="^(todo|in_progress|done)$")
@app.get("/tasks")
async def list_tasks(
page: int = 1,
limit: int = 20
) -> list[Task]:
"""
Retrieve a paginated list of tasks.
- **page**: Page number (default: 1)
- **limit**: Items per page (default: 20, max: 100)
"""
# Implementation
return []
@app.post("/tasks", status_code=201)
async def create_task(task: Task) -> Task:
"""
Create a new task.
The task will be created with the provided details.
Status defaults to 'todo' if not specified.
"""
# Implementation
return task
# FastAPI automatically generates:
# - OpenAPI schema at /openapi.json
# - Interactive Swagger UI at /docs
# - Alternative ReDoc UI at /redocWhat FastAPI Automatically Documents
- Request parameters: Query params, path params, headers extracted from function signatures
- Request body: Pydantic model converted to JSON schema with validation rules
- Response models: Return type hints become response schemas
- Status codes: Default status or explicit
status_codeparameter - Descriptions: Docstrings become endpoint descriptions
- Validation rules: Pydantic Field constraints (min/max, pattern, etc.)
Customizing API Metadata
from fastapi import FastAPI
# Customize API metadata
app = FastAPI(
title="Task Management API",
description="""
## Welcome to the Task Management API
This API allows you to manage tasks and projects efficiently.
### Features
- **Task CRUD**: Create, read, update, and delete tasks
- **Projects**: Organize tasks into projects
- **User Management**: Assign tasks to team members
- **Real-time Updates**: WebSocket support for live notifications
### Authentication
All endpoints require JWT authentication via the `Authorization` header.
### Rate Limits
- Authenticated users: 1000 requests/hour
- Unauthenticated users: 100 requests/hour
""",
version="1.0.0",
terms_of_service="https://example.com/terms",
contact={
"name": "API Support Team",
"url": "https://example.com/support",
"email": "support@example.com",
},
license_info={
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT",
},
servers=[
{
"url": "https://api.example.com",
"description": "Production server"
},
{
"url": "https://staging-api.example.com",
"description": "Staging server"
},
],
# Customize OpenAPI schema URL
openapi_url="/api/v1/openapi.json",
# Customize documentation URLs
docs_url="/api/v1/docs",
redoc_url="/api/v1/redoc",
)
# Access the generated OpenAPI schema:
# GET /api/v1/openapi.json
# Access Swagger UI:
# GET /api/v1/docs
# Access ReDoc:
# GET /api/v1/redocResult: Rich API Metadata
Your API documentation now includes:
- Markdown-formatted description with sections and links
- Contact information for support
- License details
- Multiple server environments with easy switching
- Custom URLs for OpenAPI schema and docs
Organizing with Tags
from fastapi import FastAPI
# Define tag metadata
tags_metadata = [
{
"name": "Tasks",
"description": "Operations with tasks. Create, read, update, and delete tasks.",
},
{
"name": "Projects",
"description": "Manage projects and organize tasks within them.",
},
{
"name": "Users",
"description": "User management and team collaboration features.",
"externalDocs": {
"description": "User guide",
"url": "https://docs.example.com/users",
},
},
{
"name": "Authentication",
"description": "Login, logout, and token management endpoints.",
},
]
app = FastAPI(openapi_tags=tags_metadata)
# Tag endpoints for grouping
@app.get("/tasks", tags=["Tasks"])
async def list_tasks():
"""List all tasks"""
pass
@app.post("/tasks", tags=["Tasks"])
async def create_task():
"""Create a new task"""
pass
@app.get("/projects", tags=["Projects"])
async def list_projects():
"""List all projects"""
pass
@app.post("/login", tags=["Authentication"])
async def login():
"""Authenticate and receive access token"""
pass
# Result: Documentation organized into 4 sections
# Each tag becomes a collapsible section in Swagger UIDocumenting Endpoints and Parameters
Comprehensive endpoint documentation includes descriptions, parameter details, response codes, and examples. FastAPI makes this easy with type hints and the Field() function.
Path Parameters Documentation
from fastapi import FastAPI, Path, HTTPException
app = FastAPI()
@app.get("/tasks/{task_id}")
async def get_task(
task_id: int = Path(
...,
title="Task ID",
description="The unique identifier of the task to retrieve",
ge=1, # Greater than or equal to 1
example=123
)
):
"""
Retrieve a specific task by its ID.
Returns detailed information about the task including title,
description, status, assignees, and timestamps.
"""
task = database.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
# OpenAPI documentation will show:
# - Parameter name: task_id
# - Type: integer
# - Location: path
# - Required: true
# - Validation: minimum value 1
# - Example: 123
# - Description: "The unique identifier of the task to retrieve"Query Parameters Documentation
from fastapi import FastAPI, Query
from typing import Optional
from enum import Enum
app = FastAPI()
class TaskStatus(str, Enum):
"""Valid task status values"""
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
@app.get("/tasks")
async def list_tasks(
page: int = Query(
1,
title="Page Number",
description="Page number for pagination",
ge=1,
example=1
),
limit: int = Query(
20,
title="Page Size",
description="Number of tasks per page",
ge=1,
le=100,
example=20
),
status: Optional[TaskStatus] = Query(
None,
title="Task Status",
description="Filter tasks by status",
example="in_progress"
),
search: Optional[str] = Query(
None,
title="Search Query",
description="Search tasks by title or description",
min_length=1,
max_length=100,
example="authentication"
),
sort_by: str = Query(
"created_at",
title="Sort Field",
description="Field to sort results by",
pattern="^(created_at|updated_at|title|status)$",
example="created_at"
),
order: str = Query(
"desc",
title="Sort Order",
description="Sort direction",
pattern="^(asc|desc)$",
example="desc"
)
):
"""
List tasks with filtering, searching, sorting, and pagination.
## Query Parameters
- **page**: Which page of results to retrieve
- **limit**: How many tasks per page (max 100)
- **status**: Filter by task status (todo, in_progress, done)
- **search**: Search in task title and description
- **sort_by**: Sort by field (created_at, updated_at, title, status)
- **order**: Sort direction (asc or desc)
## Example Queries
- `/tasks?status=in_progress&limit=10`
- `/tasks?search=authentication&sort_by=title`
- `/tasks?page=2&limit=50&order=asc`
"""
# Implementation
return []
# Each query parameter is fully documented with:
# - Type and validation rules
# - Default values
# - Examples
# - Description
# - Allowed values (for enums)Documentation Features
- Enums: Shows dropdown with all valid values in Swagger UI
- Validation: Min/max values, string length, regex patterns visible in docs
- Examples: Pre-filled in "Try it out" interface
- Optional params: Clearly marked as not required
- Defaults: Default values shown in documentation
Header Parameters Documentation
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/tasks")
async def list_tasks(
x_api_version: str = Header(
"1.0",
title="API Version",
description="Specify the API version to use",
example="1.0"
),
x_request_id: Optional[str] = Header(
None,
title="Request ID",
description="Optional request ID for tracing",
example="req_abc123xyz"
),
accept_language: str = Header(
"en",
title="Language",
description="Preferred language for responses",
alias="Accept-Language", # HTTP header name
example="en"
)
):
"""
List tasks with custom headers.
### Custom Headers
- **X-API-Version**: Specify API version (default: 1.0)
- **X-Request-ID**: Optional tracking ID
- **Accept-Language**: Language preference (en, es, fr)
"""
return []
# Headers appear in OpenAPI documentation
# Swagger UI provides input fields for each headerMultiple Response Codes
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get(
"/tasks/{task_id}",
responses={
200: {
"description": "Task retrieved successfully",
"content": {
"application/json": {
"example": {
"id": 123,
"title": "Implement authentication",
"status": "in_progress"
}
}
}
},
404: {
"description": "Task not found",
"content": {
"application/json": {
"example": {
"detail": "Task with ID 123 not found"
}
}
}
},
401: {
"description": "Unauthorized - Authentication required",
"content": {
"application/json": {
"example": {
"detail": "Not authenticated"
}
}
}
},
403: {
"description": "Forbidden - Insufficient permissions",
"content": {
"application/json": {
"example": {
"detail": "You don't have permission to view this task"
}
}
}
}
}
)
async def get_task(task_id: int):
"""
Retrieve a specific task.
Returns the task details if found and user has permission.
"""
# Implementation
pass
# OpenAPI docs show all possible responses
# Each status code has description and exampleRequest and Response Examples
Examples are crucial for documentation usability. They show developers exactly what request bodies should look like and what responses to expect.
Adding Examples to Pydantic Models
from pydantic import BaseModel, Field
class TaskCreate(BaseModel):
"""Schema for creating a new task"""
title: str = Field(
...,
min_length=1,
max_length=200,
description="Task title",
example="Implement JWT authentication"
)
description: str = Field(
...,
max_length=1000,
description="Detailed task description",
example="Add JWT token generation and validation to the authentication endpoints"
)
priority: int = Field(
1,
ge=1,
le=5,
description="Task priority (1=lowest, 5=highest)",
example=3
)
assignee_id: int | None = Field(
None,
description="ID of user assigned to this task",
example=42
)
# Alternative: model_config for complex examples
model_config = {
"json_schema_extra": {
"examples": [
{
"title": "Fix login bug",
"description": "Users can't log in with special characters in password",
"priority": 5,
"assignee_id": 15
},
{
"title": "Update documentation",
"description": "Add API versioning guide to docs",
"priority": 2,
"assignee_id": None
}
]
}
}
class Task(BaseModel):
"""Schema for task response"""
id: int = Field(..., description="Unique task identifier", example=123)
title: str = Field(..., description="Task title", example="Implement JWT authentication")
description: str = Field(..., description="Task description", example="Add JWT...")
status: str = Field(..., description="Task status", example="in_progress")
priority: int = Field(..., ge=1, le=5, example=3)
assignee_id: int | None = Field(None, example=42)
created_at: str = Field(..., description="Creation timestamp", example="2024-01-15T10:30:00Z")
updated_at: str = Field(..., description="Last update timestamp", example="2024-01-15T14:20:00Z")
@app.post("/tasks", response_model=Task)
async def create_task(task: TaskCreate):
"""Create a new task"""
# Implementation
pass
# Swagger UI shows:
# - Example request body pre-filled with values
# - Example response with all fields
# - "Try it out" uses examples as defaultsResult in Swagger UI
When developers click "Try it out", the request body is pre-filled:
{
"title": "Implement JWT authentication",
"description": "Add JWT token generation and validation...",
"priority": 3,
"assignee_id": 42
}Developers can edit the values or use them as-is for quick testing.
Multiple Examples per Endpoint
from fastapi import FastAPI, Body
app = FastAPI()
@app.post("/tasks")
async def create_task(
task: TaskCreate = Body(
...,
examples={
"normal": {
"summary": "Normal priority task",
"description": "A typical task with normal priority",
"value": {
"title": "Update user profile page",
"description": "Add ability to change avatar and bio",
"priority": 3,
"assignee_id": 10
}
},
"urgent": {
"summary": "High priority bug fix",
"description": "Critical bug that needs immediate attention",
"value": {
"title": "Fix payment processing error",
"description": "Users unable to complete checkout",
"priority": 5,
"assignee_id": 5
}
},
"unassigned": {
"summary": "Unassigned task",
"description": "Task without assigned developer",
"value": {
"title": "Research GraphQL implementation",
"description": "Evaluate GraphQL as alternative to REST",
"priority": 2,
"assignee_id": None
}
}
}
)
):
"""
Create a new task with various example scenarios.
The API supports different priority levels and optional assignment.
"""
pass
# Swagger UI shows dropdown with 3 example options:
# - Normal priority task
# - High priority bug fix
# - Unassigned task
# Selecting one populates the request body with that exampleResponse Examples
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get(
"/tasks/{task_id}",
responses={
200: {
"description": "Task retrieved successfully",
"content": {
"application/json": {
"examples": {
"in_progress_task": {
"summary": "Task in progress",
"value": {
"id": 123,
"title": "Implement authentication",
"description": "Add JWT auth to API",
"status": "in_progress",
"priority": 4,
"assignee_id": 10,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T14:30:00Z"
}
},
"completed_task": {
"summary": "Completed task",
"value": {
"id": 124,
"title": "Fix login bug",
"description": "Resolved password validation issue",
"status": "done",
"priority": 5,
"assignee_id": 8,
"created_at": "2024-01-14T09:00:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
}
}
}
}
},
404: {
"description": "Task not found",
"content": {
"application/json": {
"example": {
"error_code": "TASK_NOT_FOUND",
"message": "Task with ID 999 does not exist",
"status_code": 404
}
}
}
}
}
)
async def get_task(task_id: int):
"""Retrieve a task by ID"""
pass
# Documentation shows multiple example responses
# Developers see what successful and error responses look likeSchemas and Reusable Components
Reusable components eliminate duplication in your OpenAPI specification. Define schemas, parameters, responses, and security schemes once, then reference them with $ref.
Schema Inheritance with Pydantic
from pydantic import BaseModel, Field
from datetime import datetime
# Base schema with common fields
class TaskBase(BaseModel):
"""Base task schema with common fields"""
title: str = Field(..., min_length=1, max_length=200)
description: str | None = Field(None, max_length=1000)
priority: int = Field(1, ge=1, le=5)
assignee_id: int | None = None
# Schema for creating tasks (no id, timestamps)
class TaskCreate(TaskBase):
"""Schema for POST /tasks - excludes auto-generated fields"""
pass
# Schema for updating tasks (all fields optional)
class TaskUpdate(BaseModel):
"""Schema for PATCH /tasks/{id} - all fields optional"""
title: str | None = Field(None, min_length=1, max_length=200)
description: str | None = Field(None, max_length=1000)
priority: int | None = Field(None, ge=1, le=5)
assignee_id: int | None = None
status: str | None = Field(None, pattern="^(todo|in_progress|done)$")
# Full schema with all fields (for responses)
class Task(TaskBase):
"""Complete task schema for GET responses"""
id: int
status: str = Field("todo", pattern="^(todo|in_progress|done)$")
created_at: datetime
updated_at: datetime
model_config = {
"json_schema_extra": {
"example": {
"id": 123,
"title": "Implement caching",
"description": "Add Redis caching layer",
"priority": 3,
"assignee_id": 10,
"status": "in_progress",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T14:30:00Z"
}
}
}
# Paginated response wrapper
class TaskList(BaseModel):
"""Paginated list of tasks"""
tasks: list[Task]
total: int = Field(..., description="Total number of tasks")
page: int = Field(..., description="Current page number")
pages: int = Field(..., description="Total number of pages")
@app.get("/tasks", response_model=TaskList)
async def list_tasks():
"""List tasks with pagination"""
pass
@app.post("/tasks", response_model=Task, status_code=201)
async def create_task(task: TaskCreate):
"""Create a new task"""
pass
@app.get("/tasks/{task_id}", response_model=Task)
async def get_task(task_id: int):
"""Get a task by ID"""
pass
@app.patch("/tasks/{task_id}", response_model=Task)
async def update_task(task_id: int, task: TaskUpdate):
"""Update a task"""
pass
# OpenAPI schema will have 4 reusable components:
# - TaskBase (inherited by others)
# - TaskCreate (for POST requests)
# - TaskUpdate (for PATCH requests)
# - Task (for GET responses)
# - TaskList (for paginated responses)Benefits of Schema Reuse
- DRY principle: Define common fields once in base schema
- Consistency: Same fields have same validation across operations
- Smaller spec: $ref reduces OpenAPI file size
- Easier maintenance: Update schema once, affects all references
- Clear intent: Separate schemas for create/update/response show exact requirements
Nested Schemas and Relationships
from pydantic import BaseModel
from datetime import datetime
# User schema (referenced by Task)
class User(BaseModel):
id: int
username: str
email: str
avatar_url: str | None = None
# Project schema (referenced by Task)
class Project(BaseModel):
id: int
name: str
description: str | None = None
# Comment schema (nested in Task)
class Comment(BaseModel):
id: int
text: str
author_id: int
created_at: datetime
# Task with nested relationships
class TaskDetailed(BaseModel):
"""Complete task with all relationships"""
id: int
title: str
description: str | None
status: str
priority: int
# Nested user object
assignee: User | None = Field(
None,
description="User assigned to this task"
)
# Nested project object
project: Project = Field(
...,
description="Project this task belongs to"
)
# Array of nested comments
comments: list[Comment] = Field(
default_factory=list,
description="Comments on this task"
)
created_at: datetime
updated_at: datetime
@app.get("/tasks/{task_id}/detailed", response_model=TaskDetailed)
async def get_task_detailed(task_id: int):
"""
Get task with all relationships included.
Returns the task along with:
- Assigned user details
- Project information
- All comments
"""
pass
# OpenAPI schema shows nested structure:
# Task
# ├─ assignee: User
# │ ├─ id: integer
# │ ├─ username: string
# │ ├─ email: string
# │ └─ avatar_url: string
# ├─ project: Project
# │ ├─ id: integer
# │ ├─ name: string
# │ └─ description: string
# └─ comments: array[Comment]
# └─ [0]
# ├─ id: integer
# ├─ text: string
# ├─ author_id: integer
# └─ created_at: datetimeUnion Types and Discriminators
from pydantic import BaseModel, Field
from typing import Literal, Union
# Different notification types
class EmailNotification(BaseModel):
type: Literal["email"]
recipient: str = Field(..., description="Email address")
subject: str
body: str
class SMSNotification(BaseModel):
type: Literal["sms"]
phone_number: str = Field(..., description="Phone number with country code")
message: str = Field(..., max_length=160)
class PushNotification(BaseModel):
type: Literal["push"]
device_token: str
title: str
body: str
badge: int | None = None
# Union with discriminator
Notification = Union[EmailNotification, SMSNotification, PushNotification]
@app.post("/notifications/send")
async def send_notification(
notification: Notification = Field(..., discriminator="type")
):
"""
Send a notification via email, SMS, or push.
The `type` field determines which notification schema to use:
- **email**: Sends email notification
- **sms**: Sends SMS notification
- **push**: Sends push notification
"""
if notification.type == "email":
# Send email
pass
elif notification.type == "sms":
# Send SMS
pass
elif notification.type == "push":
# Send push notification
pass
# OpenAPI shows discriminator:
# - "type" field determines which schema
# - Swagger UI provides dropdown to select type
# - Schema changes based on selectionAuthentication and Security Schemes
Document authentication requirements so developers know how to authenticate API requests. OpenAPI supports multiple security schemes: API keys, HTTP authentication, OAuth2, and OpenID Connect.
JWT Bearer Token Authentication
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
app = FastAPI()
# Define security scheme
security = HTTPBearer(
scheme_name="JWT Bearer Token",
description="Enter your JWT access token"
)
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify JWT token"""
token = credentials.credentials
# Verify token (simplified)
if not is_valid_token(token):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
return token
@app.get("/tasks", dependencies=[Depends(verify_token)])
async def list_tasks():
"""
List all tasks (requires authentication).
**Authentication**: This endpoint requires a valid JWT token in the
Authorization header: `Authorization: Bearer <token>`
"""
return []
@app.get("/tasks/{task_id}", dependencies=[Depends(verify_token)])
async def get_task(task_id: int):
"""Get a task by ID (requires authentication)"""
pass
# Swagger UI shows:
# - Lock icon on protected endpoints
# - "Authorize" button in top right
# - Token input field
# - After authorization, token automatically included in requestsHow It Works in Swagger UI
- Developer clicks "Authorize" button
- Modal opens asking for JWT token
- Developer pastes token (e.g., from login endpoint response)
- Swagger UI stores token
- All subsequent requests automatically include
Authorization: Bearer <token>header
OAuth2 with Scopes
from fastapi import FastAPI, Depends, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
app = FastAPI()
# Define OAuth2 scheme with scopes
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"tasks:read": "Read tasks",
"tasks:write": "Create and update tasks",
"tasks:delete": "Delete tasks",
"projects:read": "Read projects",
"projects:write": "Create and update projects",
"admin": "Full administrative access",
}
)
async def verify_scopes(
security_scopes: SecurityScopes,
token: str = Depends(oauth2_scheme)
):
"""Verify token has required scopes"""
# Decode token and extract scopes
token_scopes = get_token_scopes(token)
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Not enough permissions. Required scope: {scope}"
)
return token
@app.get(
"/tasks",
dependencies=[Security(verify_scopes, scopes=["tasks:read"])]
)
async def list_tasks():
"""
List tasks (requires `tasks:read` scope).
**Required Permission**: tasks:read
"""
return []
@app.post(
"/tasks",
dependencies=[Security(verify_scopes, scopes=["tasks:write"])]
)
async def create_task():
"""
Create task (requires `tasks:write` scope).
**Required Permission**: tasks:write
"""
pass
@app.delete(
"/tasks/{task_id}",
dependencies=[Security(verify_scopes, scopes=["tasks:delete"])]
)
async def delete_task(task_id: int):
"""
Delete task (requires `tasks:delete` scope).
**Required Permission**: tasks:delete
"""
pass
# OpenAPI documentation shows:
# - Required scopes for each endpoint
# - Scope descriptions
# - Swagger UI provides checkbox for each scope during authorizationAPI Key Authentication
from fastapi import FastAPI, Security, HTTPException, status
from fastapi.security import APIKeyHeader
app = FastAPI()
# API key in header
api_key_header = APIKeyHeader(
name="X-API-Key",
scheme_name="API Key",
description="API key for authentication"
)
async def verify_api_key(api_key: str = Security(api_key_header)):
"""Verify API key"""
if api_key not in VALID_API_KEYS:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid API key"
)
return api_key
@app.get("/tasks", dependencies=[Depends(verify_api_key)])
async def list_tasks():
"""
List tasks (requires API key).
**Authentication**: Include your API key in the `X-API-Key` header.
Example:
```
curl -H "X-API-Key: your_api_key_here" https://api.example.com/tasks
```
"""
return []
# Swagger UI provides:
# - Input field for API key
# - Automatic inclusion in X-API-Key header
# - Lock icon on protected endpointsMultiple Security Schemes
from fastapi import FastAPI, Depends, Security
from fastapi.security import HTTPBearer, APIKeyHeader
app = FastAPI()
# Define multiple security schemes
bearer_scheme = HTTPBearer()
api_key_scheme = APIKeyHeader(name="X-API-Key")
async def verify_bearer_or_api_key(
bearer: HTTPAuthorizationCredentials | None = Security(bearer_scheme, auto_error=False),
api_key: str | None = Security(api_key_scheme, auto_error=False)
):
"""Accept either Bearer token or API key"""
if bearer:
# Verify JWT token
return verify_jwt(bearer.credentials)
elif api_key:
# Verify API key
return verify_api_key_func(api_key)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required. Provide either Bearer token or API key."
)
@app.get("/tasks", dependencies=[Depends(verify_bearer_or_api_key)])
async def list_tasks():
"""
List tasks (requires Bearer token OR API key).
**Authentication Methods**:
1. JWT Bearer token: `Authorization: Bearer <token>`
2. API key: `X-API-Key: <key>`
"""
return []
# Swagger UI shows both authentication options
# Developer can choose which method to useSwagger UI and ReDoc Customization
Swagger UI and ReDoc are the two most popular OpenAPI documentation viewers. Both are automatically generated by FastAPI, and both can be customized to match your branding.
Customizing Swagger UI
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
# Mount custom static files (for custom logo, CSS)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
"""Custom Swagger UI with branding"""
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",
# Custom CSS for branding
swagger_ui_parameters={
"deepLinking": True,
"displayRequestDuration": True,
"filter": True, # Enable search
"tryItOutEnabled": True, # Enable "Try it out" by default
"syntaxHighlight.theme": "monokai",
# Persist authorization
"persistAuthorization": True,
# Default expanded tags
"docExpansion": "list", # none, list, or full
}
)
# Alternative: Inject custom CSS without replacing Swagger UI
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui():
return get_swagger_ui_html(
openapi_url="/openapi.json",
title="API Docs",
swagger_ui_parameters={
# Custom CSS
"customCss": """
.swagger-ui .topbar {
background-color: #17724d;
}
.swagger-ui .info .title {
color: #17724d;
}
.swagger-ui .btn.execute {
background-color: #17724d;
border-color: #17724d;
}
"""
}
)Swagger UI Customization Options
- deepLinking: Enable deep linking for easy sharing of specific operations
- displayRequestDuration: Show how long each request takes
- filter: Add search box to filter operations
- persistAuthorization: Remember authorization between page reloads
- docExpansion: Control whether operations are expanded by default
- customCss: Override styles to match brand colors
Customizing ReDoc
from fastapi import FastAPI
from fastapi.openapi.docs import get_redoc_html
app = FastAPI(redoc_url=None) # Disable default ReDoc
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
"""Custom ReDoc with theme"""
return get_redoc_html(
openapi_url="/openapi.json",
title="API Documentation - ReDoc",
redoc_favicon_url="/static/favicon.png",
# Custom options
redoc_js_url="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js",
with_google_fonts=True,
)
# Alternative: Custom theme with options
@app.get("/redoc", include_in_schema=False)
async def custom_redoc():
return get_redoc_html(
openapi_url="/openapi.json",
title="API Docs",
redoc_options={
# Theme customization
"theme": {
"colors": {
"primary": {
"main": "#17724d"
}
},
"typography": {
"fontSize": "16px",
"fontFamily": "system-ui, -apple-system, sans-serif",
"headings": {
"fontFamily": "system-ui, -apple-system, sans-serif"
},
"code": {
"fontSize": "14px",
"fontFamily": "Monaco, monospace"
}
},
"sidebar": {
"backgroundColor": "#fafafa",
"textColor": "#333333"
},
"rightPanel": {
"backgroundColor": "#1e1e1e",
"textColor": "#ffffff"
}
},
# Display options
"hideDownloadButton": False,
"disableSearch": False,
"expandResponses": "200,201",
"jsonSampleExpandLevel": 2,
"hideSingleRequestSampleTab": True,
"menuToggle": True,
"scrollYOffset": 50,
"pathInMiddlePanel": True,
"requiredPropsFirst": True,
"sortPropsAlphabetically": True,
"payloadSampleIdx": 0,
}
)Comparison: Swagger UI vs ReDoc
Swagger UI
Best for: Interactive testing
- ✓ "Try it out" for testing
- ✓ Built-in authorization
- ✓ Execute requests directly
- ✗ Cluttered for large APIs
- ✗ Less polished design
ReDoc
Best for: Documentation reading
- ✓ Beautiful, clean design
- ✓ Three-column layout
- ✓ Better for large APIs
- ✗ No built-in testing
- ✗ Read-only
Best Practice: Provide Both
Offer both Swagger UI and ReDoc to serve different use cases:
- /docs → Swagger UI for developers who want to test endpoints interactively
- /redoc → ReDoc for developers who want to read comprehensive documentation
- Both are generated from the same OpenAPI spec, so no extra work!
Adding Custom Documentation Pages
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/docs/guides/authentication", include_in_schema=False)
async def authentication_guide():
"""Custom guide page for authentication"""
return HTMLResponse(content="""
<!DOCTYPE html>
<html>
<head>
<title>Authentication Guide</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 40px; }
h1 { color: #17724d; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Authentication Guide</h1>
<p>This guide explains how to authenticate with our API.</p>
<h2>Step 1: Obtain an API Key</h2>
<p>Sign up at <a href="/signup">example.com/signup</a> to receive your API key.</p>
<h2>Step 2: Include in Requests</h2>
<p>Add your API key to the <code>X-API-Key</code> header:</p>
<pre>curl -H "X-API-Key: your_key_here" https://api.example.com/tasks</pre>
<h2>Step 3: Handling Errors</h2>
<p>If your key is invalid, you'll receive a 403 error.</p>
<p><a href="/docs">← Back to API Documentation</a></p>
</body>
</html>
""")
@app.get("/docs/guides/rate-limits", include_in_schema=False)
async def rate_limits_guide():
"""Custom guide page for rate limits"""
return HTMLResponse(content="""...""")
# Link to custom guides from API description:
app = FastAPI(
description="""
## API Documentation
**Getting Started Guides**:
- [Authentication Guide](/docs/guides/authentication)
- [Rate Limits](/docs/guides/rate-limits)
- [Error Handling](/docs/guides/errors)
**Interactive Documentation**:
- [Swagger UI](/docs) - Test endpoints interactively
- [ReDoc](/redoc) - Read comprehensive docs
"""
)Code Generation and Documentation Best Practices
OpenAPI enables code generation for client SDKs, server stubs, and testing tools. Your documentation becomes a source of truth that can generate production-ready code.
Generating Client SDKs from OpenAPI
Popular Code Generation Tools
OpenAPI Generator
Generates clients in 50+ languages
openapi-generator-cli generate -i openapi.json -g python -o ./clientSwagger Codegen
Official Swagger code generator
swagger-codegen generate -i openapi.json -l python -o ./client# Step 1: Export OpenAPI schema
# GET https://api.example.com/openapi.json > openapi.json
# Step 2: Generate Python client
openapi-generator-cli generate \
-i openapi.json \
-g python \
-o ./generated-client \
--additional-properties=packageName=task_api_client
# Step 3: Install generated client
cd generated-client
pip install -e .
# Step 4: Use generated client
from task_api_client import ApiClient, Configuration, TasksApi
# Configure client
config = Configuration(
host="https://api.example.com",
api_key={"X-API-Key": "your_api_key_here"}
)
# Create API client
with ApiClient(config) as api_client:
tasks_api = TasksApi(api_client)
# List tasks
response = tasks_api.list_tasks(page=1, limit=20)
print(f"Found {response.total} tasks")
# Create task
new_task = tasks_api.create_task(
task_create={
"title": "Generated from SDK",
"description": "Created using auto-generated client",
"priority": 3
}
)
print(f"Created task {new_task.id}")
# Get specific task
task = tasks_api.get_task(task_id=new_task.id)
print(f"Task status: {task.status}")
# Generated client includes:
# - Type-safe methods for all endpoints
# - Automatic request/response serialization
# - Error handling
# - Authentication handling
# - Retry logic (configurable)
# - Request/response models with validationBenefits of Generated Clients
- Type safety: Auto-complete and type checking in IDEs
- Consistency: Same API experience across languages
- Reduced errors: Request/response validation built-in
- Faster integration: No manual HTTP client code
- Always up-to-date: Regenerate when API changes
- Documentation: Docstrings from OpenAPI descriptions
API Versioning in Documentation
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
# Version 1 of the API
app_v1 = FastAPI(
title="Task Management API",
version="1.0.0",
openapi_url="/api/v1/openapi.json",
docs_url="/api/v1/docs",
redoc_url="/api/v1/redoc",
)
@app_v1.get("/tasks")
async def list_tasks_v1():
"""List tasks (v1 format)"""
return [{"id": 1, "name": "Task 1"}] # Old format
# Version 2 of the API (with breaking changes)
app_v2 = FastAPI(
title="Task Management API",
version="2.0.0",
description="""
## Version 2.0.0 Changes
**Breaking Changes**:
- `name` field renamed to `title`
- Added `description` field
- Task status is now enum instead of boolean
**Migration Guide**: [View migration guide](/api/v2/migration)
""",
openapi_url="/api/v2/openapi.json",
docs_url="/api/v2/docs",
redoc_url="/api/v2/redoc",
)
@app_v2.get("/tasks")
async def list_tasks_v2():
"""List tasks (v2 format with new fields)"""
return [{
"id": 1,
"title": "Task 1", # Renamed from 'name'
"description": "Task description", # New field
"status": "in_progress" # New enum status
}]
# Main app that mounts both versions
from fastapi import FastAPI
app = FastAPI()
app.mount("/api/v1", app_v1)
app.mount("/api/v2", app_v2)
# Landing page with version selector
@app.get("/", include_in_schema=False)
async def root():
return JSONResponse({
"message": "Task Management API",
"versions": {
"v1": {
"status": "deprecated",
"docs": "/api/v1/docs",
"sunset_date": "2025-12-31"
},
"v2": {
"status": "current",
"docs": "/api/v2/docs"
}
}
})
# Result: Two separate documentation sites
# - /api/v1/docs - Version 1 (deprecated)
# - /api/v2/docs - Version 2 (current)
# Each with its own OpenAPI schemaDocumentation Best Practices
DO
- Write clear, concise descriptions
- Include realistic examples
- Document all error responses
- Explain rate limits and quotas
- Provide authentication guide
- Keep docs in sync with code
- Version your documentation
- Add changelog for updates
DON'T
- Leave descriptions empty
- Use technical jargon without explanation
- Forget to document edge cases
- Use "TODO" or placeholder text
- Document internal endpoints
- Let docs become outdated
- Skip example responses
- Expose sensitive information
Maintaining API Changelogs
# Add changelog to API description
app = FastAPI(
title="Task Management API",
version="2.1.0",
description="""
## Task Management API
Comprehensive API for managing tasks, projects, and teams.
---
## Changelog
### Version 2.1.0 (2024-01-20)
**Added**:
- New `/tasks/batch` endpoint for bulk operations
- Support for task templates
- WebSocket endpoint for real-time updates
**Changed**:
- Increased max page size from 100 to 200
- Improved search algorithm performance
**Fixed**:
- Fixed pagination issue with filtered results
- Corrected timestamp format in notifications
**Deprecated**:
- `GET /tasks/legacy` - Use `GET /tasks` instead (removal: 2024-06-30)
---
### Version 2.0.0 (2023-12-01)
**Breaking Changes**:
- Renamed `name` field to `title` in Task schema
- Changed date format from `DD/MM/YYYY` to ISO 8601
- Removed deprecated `/v1` endpoints
**Added**:
- OAuth2 authentication support
- Task priority levels (1-5)
- Project grouping
**Migration Guide**: [View v1 to v2 migration guide](/docs/migration-v1-v2)
---
### Version 1.2.0 (2023-09-15)
**Added**:
- Task comments
- File attachments
- Email notifications
[View full changelog](/docs/changelog)
"""
)
# Separate changelog endpoint
@app.get("/docs/changelog", include_in_schema=False)
async def changelog():
"""Detailed changelog with examples"""
return HTMLResponse(content="""
<!DOCTYPE html>
<html>
<head>
<title>API Changelog</title>
<style>
body { font-family: system-ui; max-width: 1000px; margin: 0 auto; padding: 40px; }
h1 { color: #17724d; }
.version { border-left: 4px solid #17724d; padding-left: 20px; margin-bottom: 40px; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.added { background: #dcfce7; color: #15803d; }
.changed { background: #dbeafe; color: #1e40af; }
.deprecated { background: #fef3c7; color: #92400e; }
.removed { background: #fee2e2; color: #dc2626; }
.fixed { background: #e9d5ff; color: #6b21a8; }
</style>
</head>
<body>
<h1>API Changelog</h1>
<div class="version">
<h2>Version 2.1.0 <small>(2024-01-20)</small></h2>
<h3><span class="badge added">ADDED</span> Bulk Operations</h3>
<p>New endpoint for creating, updating, or deleting multiple tasks in one request:</p>
<pre>POST /api/tasks/batch
{
"operations": [
{"action": "create", "data": {...}},
{"action": "update", "id": 123, "data": {...}}
]
}</pre>
<h3><span class="badge changed">CHANGED</span> Increased Page Size Limit</h3>
<p>Maximum page size increased from 100 to 200 items.</p>
<h3><span class="badge fixed">FIXED</span> Pagination Bug</h3>
<p>Fixed issue where pagination returned duplicate items when using filters.</p>
<h3><span class="badge deprecated">DEPRECATED</span> Legacy Endpoint</h3>
<p><code>GET /tasks/legacy</code> will be removed on 2024-06-30. Migrate to <code>GET /tasks</code>.</p>
</div>
<div class="version">
<h2>Version 2.0.0 <small>(2023-12-01)</small></h2>
<h3><span class="badge removed">BREAKING</span> Field Renamed</h3>
<p>The <code>name</code> field has been renamed to <code>title</code> in all task endpoints.</p>
<p><strong>Before (v1):</strong></p>
<pre>{"id": 1, "name": "My Task"}</pre>
<p><strong>After (v2):</strong></p>
<pre>{"id": 1, "title": "My Task"}</pre>
</div>
<p><a href="/docs">← Back to API Documentation</a></p>
</body>
</html>
""")Documentation as a Product
Treat documentation as a product, not an afterthought:
- Test your examples: All code examples should actually work
- Get feedback: Ask developers what's confusing
- Measure usage: Track which endpoints are accessed via docs
- Iterate: Continuously improve based on support questions
- Automate: Generate docs from code, keep them in sync
- Make it searchable: Good search is critical for large APIs
Final Checklist: Great API Documentation
- All endpoints documented with descriptions
- All parameters explained (path, query, header, body)
- All response codes documented with examples
- Error responses include error codes and messages
- Authentication methods clearly explained
- Descriptions are clear and concise
- Technical jargon is explained
- Examples show realistic use cases
- Edge cases and limitations are documented
- Related endpoints are cross-referenced
- Interactive documentation available (Swagger UI)
- Code examples in multiple languages
- Getting started guide for new users
- Searchable (for large APIs)
- Mobile-friendly (responsive design)
- Documentation auto-generated from code
- Versioned alongside API versions
- Changelog maintained for each release
- Deprecated endpoints clearly marked
- Regular reviews to catch outdated info