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
FieldDescriptionRequired
openapiOpenAPI Specification version (e.g., "3.0.3", "3.1.0")Yes
infoAPI metadata (title, version, description, contact, license)Yes
serversArray of server URLs (production, staging, development)No
pathsAvailable endpoints and their operationsYes
componentsReusable schemas, parameters, responses, security schemesNo
securityGlobal security requirementsNo
tagsGroup endpoints by categoryNo
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: todo
Key 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 examples field
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 /redoc
What 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_code parameter
  • 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/redoc
Result: 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 UI

Documenting 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 header
Multiple 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 example

Request 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 defaults
Result 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 example
Response 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 like

Schemas 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: datetime
Union 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 selection

Authentication 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 requests
How It Works in Swagger UI
  1. Developer clicks "Authorize" button
  2. Modal opens asking for JWT token
  3. Developer pastes token (e.g., from login endpoint response)
  4. Swagger UI stores token
  5. 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 authorization
API 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 endpoints
Multiple 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 use

Swagger 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 ./client
Swagger 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 validation
Benefits 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 schema
Documentation 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