Back to blog

Understanding OpenAPI with FastAPI: Auto-Generated Documentation

pythonfastapiapiopenapidocumentation
Understanding OpenAPI with FastAPI: Auto-Generated Documentation

One of FastAPI's most powerful features is its automatic OpenAPI documentation generation. The moment you define your API endpoints, FastAPI creates comprehensive, interactive documentation that follows the OpenAPI (formerly Swagger) specification. Let's explore how this works and how to leverage it effectively.

What is OpenAPI?

OpenAPI (formerly known as Swagger) is a specification for describing RESTful APIs. It provides a standard way to document your API's endpoints, request/response formats, authentication methods, and more.

Key Benefits:

  • Standardization - Industry-standard format understood by tools worldwide
  • Automation - Generate client SDKs, server stubs, and tests automatically
  • Documentation - Interactive docs that developers can test immediately
  • Validation - Ensure API contracts are followed
  • Discoverability - Make APIs easy to understand and consume

FastAPI's Automatic OpenAPI Generation

FastAPI generates OpenAPI 3.0+ specifications automatically from your Python type hints and Pydantic models. No manual documentation writing required!

Example: Basic Endpoint

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class User(BaseModel):
    name: str
    email: str
    age: int
 
@app.post("/users/")
async def create_user(user: User):
    return {"message": f"Created user {user.name}"}

What FastAPI Generates:

  • OpenAPI schema with endpoint definition
  • Request body schema from User model
  • Response schema from return type
  • Interactive docs at /docs (Swagger UI)
  • Alternative docs at /redoc (ReDoc)
  • OpenAPI JSON at /openapi.json

Accessing the Documentation

Once your FastAPI app is running, you get three documentation endpoints automatically:

1. Swagger UI (/docs)

# Start your app
uvicorn main:app --reload
 
# Visit: http://localhost:8000/docs

Features:

  • Interactive interface to test endpoints
  • Try out API calls directly in browser
  • See request/response examples
  • OAuth2 authentication support

2. ReDoc (/redoc)

# Visit: http://localhost:8000/redoc

Features:

  • Clean, professional documentation layout
  • Three-panel design with navigation
  • Better for reading than testing
  • Great for sharing with stakeholders

3. OpenAPI JSON (/openapi.json)

# Visit: http://localhost:8000/openapi.json
# Or with curl:
curl http://localhost:8000/openapi.json

Use Cases:

  • Generate client SDKs with openapi-generator
  • Import into Postman or Insomnia
  • Validate API contracts
  • Share machine-readable API specs

Customizing API Metadata

Make your documentation more professional with metadata:

from fastapi import FastAPI
 
app = FastAPI(
    title="My Awesome API",
    description="This API does awesome things with data",
    version="1.0.0",
    terms_of_service="http://example.com/terms/",
    contact={
        "name": "API Support",
        "url": "http://example.com/support",
        "email": "support@example.com",
    },
    license_info={
        "name": "Apache 2.0",
        "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
    },
)

Result: All this metadata appears in your OpenAPI docs, making your API look professional and production-ready.

Documenting Endpoints

Basic Documentation

from fastapi import FastAPI
 
app = FastAPI()
 
@app.get(
    "/items/{item_id}",
    summary="Get an item by ID",
    description="Retrieve a specific item from the database using its unique identifier.",
    response_description="The requested item",
)
async def read_item(item_id: int):
    """
    Additional details about the endpoint.
    
    You can use markdown here:
    - Point 1
    - Point 2
    """
    return {"item_id": item_id}

Tags for Organization

Group related endpoints with tags:

@app.get("/users/", tags=["users"])
async def get_users():
    return []
 
@app.post("/users/", tags=["users"])
async def create_user():
    return {}
 
@app.get("/products/", tags=["products"])
async def get_products():
    return []

Result: Swagger UI groups endpoints by tags, making navigation easier.

Advanced Tag Metadata

from fastapi import FastAPI
 
tags_metadata = [
    {
        "name": "users",
        "description": "Operations with users. Login, registration, etc.",
    },
    {
        "name": "items",
        "description": "Manage items. CRUD operations.",
        "externalDocs": {
            "description": "Items external docs",
            "url": "https://example.com/items-docs",
        },
    },
]
 
app = FastAPI(openapi_tags=tags_metadata)

Schema Customization

Custom Response Examples

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class Item(BaseModel):
    name: str
    price: float
    description: str | None = None
 
    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "name": "Laptop",
                    "price": 999.99,
                    "description": "A powerful laptop"
                }
            ]
        }
    }
 
@app.post("/items/")
async def create_item(item: Item):
    return item

Result: Swagger UI shows your example in the request body schema.

Field Descriptions

from pydantic import BaseModel, Field
 
class User(BaseModel):
    username: str = Field(
        ..., 
        description="Unique username for the account",
        min_length=3,
        max_length=50
    )
    email: str = Field(
        ..., 
        description="User's email address",
        pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$"
    )
    age: int = Field(
        ..., 
        description="User's age in years",
        ge=0,
        le=150
    )

Multiple Response Models

Document different response types:

from fastapi import FastAPI, status
from pydantic import BaseModel
 
app = FastAPI()
 
class Item(BaseModel):
    name: str
    price: float
 
class ErrorResponse(BaseModel):
    detail: str
 
@app.post(
    "/items/",
    response_model=Item,
    status_code=status.HTTP_201_CREATED,
    responses={
        201: {
            "description": "Item created successfully",
            "model": Item,
        },
        400: {
            "description": "Invalid input",
            "model": ErrorResponse,
        },
        409: {
            "description": "Item already exists",
            "model": ErrorResponse,
        },
    },
)
async def create_item(item: Item):
    return item

Deprecating Endpoints

Mark endpoints as deprecated:

@app.get("/old-endpoint/", deprecated=True)
async def old_endpoint():
    """This endpoint is deprecated. Use /new-endpoint/ instead."""
    return {"message": "This is deprecated"}
 
@app.get("/new-endpoint/")
async def new_endpoint():
    """Use this endpoint for new implementations."""
    return {"message": "This is the new way"}

Result: Deprecated endpoints are clearly marked in documentation.

Security Schemes

OAuth2 Documentation

from fastapi import FastAPI, Depends
from fastapi.security import OAuth2PasswordBearer
 
app = FastAPI()
 
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
@app.get("/protected/")
async def protected_route(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Result: Swagger UI adds an "Authorize" button for OAuth2 authentication.

API Key Documentation

from fastapi import FastAPI, Security
from fastapi.security import APIKeyHeader
 
app = FastAPI()
 
api_key_header = APIKeyHeader(name="X-API-Key")
 
@app.get("/secure/")
async def secure_route(api_key: str = Security(api_key_header)):
    return {"api_key": api_key}

Multiple Security Schemes

from fastapi import FastAPI, Depends, Security
from fastapi.security import OAuth2PasswordBearer, APIKeyHeader
 
app = FastAPI()
 
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
api_key_header = APIKeyHeader(name="X-API-Key")
 
@app.get("/protected/")
async def protected_route(
    token: str = Depends(oauth2_scheme),
    api_key: str = Security(api_key_header)
):
    """Requires both OAuth2 token and API key."""
    return {"message": "Access granted"}

Customizing OpenAPI Schema

Custom OpenAPI Schema

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
 
app = FastAPI()
 
def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
        
    openapi_schema = get_openapi(
        title="Custom API",
        version="2.5.0",
        description="This is a custom OpenAPI schema",
        routes=app.routes,
    )
    
    # Add custom extensions
    openapi_schema["info"]["x-logo"] = {
        "url": "https://example.com/logo.png"
    }
    
    app.openapi_schema = openapi_schema
    return app.openapi_schema
 
app.openapi = custom_openapi

Hiding Endpoints

Hide specific endpoints from documentation:

@app.get("/internal/", include_in_schema=False)
async def internal_endpoint():
    """This won't appear in OpenAPI docs."""
    return {"message": "Internal use only"}

Request Body Examples

Multiple Examples

from fastapi import FastAPI, Body
from pydantic import BaseModel
 
app = FastAPI()
 
class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
 
@app.post("/items/")
async def create_item(
    item: Item = Body(
        examples={
            "normal": {
                "summary": "A normal example",
                "description": "A typical item",
                "value": {
                    "name": "Laptop",
                    "description": "A powerful laptop",
                    "price": 999.99,
                },
            },
            "minimal": {
                "summary": "Minimal example",
                "description": "Minimal required fields",
                "value": {
                    "name": "Mouse",
                    "price": 19.99,
                },
            },
            "invalid": {
                "summary": "Invalid example",
                "description": "This will fail validation",
                "value": {
                    "name": "Keyboard",
                    "price": -10,
                },
            },
        }
    )
):
    return item

Path Operation Configuration

Complete Example

from fastapi import FastAPI, status, Path, Query
from pydantic import BaseModel
from typing import List
 
app = FastAPI()
 
class Item(BaseModel):
    name: str
    price: float
 
@app.get(
    "/items/{item_id}",
    response_model=Item,
    status_code=status.HTTP_200_OK,
    tags=["items"],
    summary="Get an item",
    description="Retrieve a single item by its ID",
    response_description="The item details",
    deprecated=False,
    operation_id="get_item_by_id",
    responses={
        200: {"description": "Successful Response"},
        404: {"description": "Item not found"},
        422: {"description": "Validation Error"},
    },
)
async def get_item(
    item_id: int = Path(..., description="The ID of the item to get", ge=1),
    include_details: bool = Query(False, description="Include detailed information"),
):
    """
    Get an item by ID.
    
    **Parameters:**
    - **item_id**: The unique identifier of the item
    - **include_details**: Whether to include detailed information
    
    **Returns:**
    - The item object with name and price
    """
    return Item(name="Sample Item", price=99.99)

OpenAPI Extensions

Custom Extensions

from fastapi import FastAPI
 
app = FastAPI()
 
@app.get("/items/")
async def get_items():
    return []
 
# Modify OpenAPI schema to add custom extensions
def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    
    openapi_schema = get_openapi(
        title="API with Extensions",
        version="1.0.0",
        routes=app.routes,
    )
    
    # Add custom extension to operation
    openapi_schema["paths"]["/items/"]["get"]["x-custom-field"] = "custom-value"
    
    app.openapi_schema = openapi_schema
    return app.openapi_schema
 
app.openapi = custom_openapi

Generating Client SDKs

Use the OpenAPI spec to generate client libraries:

JavaScript/TypeScript

# Install openapi-generator
npm install @openapitools/openapi-generator-cli -g
 
# Generate TypeScript client
openapi-generator-cli generate \
  -i http://localhost:8000/openapi.json \
  -g typescript-axios \
  -o ./client-typescript

Python

# Generate Python client
openapi-generator-cli generate \
  -i http://localhost:8000/openapi.json \
  -g python \
  -o ./client-python

Go

# Generate Go client
openapi-generator-cli generate \
  -i http://localhost:8000/openapi.json \
  -g go \
  -o ./client-go

Real-World Example: E-commerce API

Here's a complete example with proper OpenAPI documentation:

from fastapi import FastAPI, HTTPException, status, Query, Path
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime
 
# API Metadata
app = FastAPI(
    title="E-commerce API",
    description="A comprehensive e-commerce API with product and order management",
    version="1.0.0",
    contact={
        "name": "API Support",
        "email": "support@example.com",
    },
    license_info={
        "name": "MIT",
    },
)
 
# Models
class Product(BaseModel):
    id: int = Field(..., description="Unique product identifier")
    name: str = Field(..., description="Product name", min_length=1, max_length=100)
    description: str = Field(..., description="Product description")
    price: float = Field(..., description="Product price", gt=0)
    stock: int = Field(..., description="Available stock", ge=0)
    
    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "id": 1,
                    "name": "Laptop Pro 15",
                    "description": "High-performance laptop",
                    "price": 1299.99,
                    "stock": 50
                }
            ]
        }
    }
 
class Order(BaseModel):
    id: int = Field(..., description="Unique order identifier")
    product_id: int = Field(..., description="Product ID being ordered")
    quantity: int = Field(..., description="Order quantity", ge=1)
    total: float = Field(..., description="Total order amount")
    created_at: datetime = Field(default_factory=datetime.now)
 
# Endpoints
@app.get(
    "/products/",
    response_model=List[Product],
    tags=["products"],
    summary="List all products",
    description="Retrieve a list of all available products with pagination support",
)
async def list_products(
    skip: int = Query(0, ge=0, description="Number of items to skip"),
    limit: int = Query(10, ge=1, le=100, description="Maximum number of items to return"),
):
    """
    Get a paginated list of products.
    
    Use the skip and limit parameters to paginate through results.
    """
    # Mock data
    return [
        Product(
            id=1,
            name="Laptop",
            description="High-performance laptop",
            price=999.99,
            stock=10
        )
    ]
 
@app.get(
    "/products/{product_id}",
    response_model=Product,
    tags=["products"],
    summary="Get a product by ID",
    responses={
        200: {"description": "Product found"},
        404: {"description": "Product not found"},
    },
)
async def get_product(
    product_id: int = Path(..., description="The ID of the product", ge=1)
):
    """
    Retrieve detailed information about a specific product.
    """
    return Product(
        id=product_id,
        name="Sample Product",
        description="A sample product",
        price=99.99,
        stock=100
    )
 
@app.post(
    "/orders/",
    response_model=Order,
    status_code=status.HTTP_201_CREATED,
    tags=["orders"],
    summary="Create a new order",
    responses={
        201: {"description": "Order created successfully"},
        400: {"description": "Invalid input"},
        404: {"description": "Product not found"},
    },
)
async def create_order(
    product_id: int = Query(..., description="Product ID to order", ge=1),
    quantity: int = Query(..., description="Quantity to order", ge=1),
):
    """
    Create a new order for a product.
    
    The order will be created if the product exists and has sufficient stock.
    """
    # Mock order creation
    return Order(
        id=1,
        product_id=product_id,
        quantity=quantity,
        total=999.99 * quantity
    )

Best Practices

1. Use Descriptive Names

# ❌ Bad
@app.get("/data/")
async def get():
    pass
 
# ✅ Good
@app.get("/users/")
async def get_all_users():
    """Retrieve all users from the database."""
    pass

2. Provide Examples

# ✅ Always provide examples in models
class User(BaseModel):
    name: str
    email: str
    
    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "name": "John Doe",
                    "email": "john@example.com"
                }
            ]
        }
    }

3. Document All Responses

# ✅ Document both success and error responses
@app.get(
    "/items/{item_id}",
    responses={
        200: {"description": "Success"},
        404: {"description": "Item not found"},
        422: {"description": "Validation error"},
    }
)
async def get_item(item_id: int):
    pass

4. Use Tags for Organization

# ✅ Group related endpoints
@app.get("/users/", tags=["users"])
async def get_users(): pass
 
@app.post("/users/", tags=["users"])
async def create_user(): pass

5. Add Descriptions to Fields

# ✅ Describe what each field represents
class Product(BaseModel):
    name: str = Field(..., description="Product name")
    price: float = Field(..., description="Price in USD", gt=0)

Common Pitfalls

1. Missing Response Models

# ❌ Bad - No response model
@app.get("/users/")
async def get_users():
    return [{"name": "John"}]
 
# ✅ Good - Explicit response model
@app.get("/users/", response_model=List[User])
async def get_users():
    return [User(name="John", email="john@example.com")]

2. Inconsistent Naming

# ❌ Bad - Inconsistent
@app.get("/getUsers/")
@app.post("/CreateUser/")
@app.delete("/delete_user/")
 
# ✅ Good - Consistent REST conventions
@app.get("/users/")
@app.post("/users/")
@app.delete("/users/{user_id}")

3. Missing Error Documentation

# ❌ Bad - No error docs
@app.get("/items/{item_id}")
async def get_item(item_id: int):
    pass
 
# ✅ Good - Document errors
@app.get(
    "/items/{item_id}",
    responses={
        404: {"description": "Item not found"},
        422: {"description": "Invalid item ID"},
    }
)
async def get_item(item_id: int):
    pass

Advanced: Webhooks Documentation

FastAPI 0.99.0+ supports webhook documentation:

from fastapi import FastAPI
 
app = FastAPI()
 
@app.webhooks.post("new-user")
async def new_user_webhook(user: User):
    """
    This webhook is triggered when a new user is created.
    
    Your server should implement this endpoint to receive notifications.
    """
    pass

Conclusion

FastAPI's automatic OpenAPI generation is a game-changer for API development. By leveraging Python's type hints and Pydantic models, you get:

  • Automatic Documentation - No manual writing required
  • Interactive Testing - Swagger UI and ReDoc out of the box
  • Type Safety - Validation and serialization handled automatically
  • Client Generation - Generate SDKs for any language
  • Industry Standard - OpenAPI 3.0+ compatibility

The key is to use FastAPI's features properly: provide examples, document responses, organize with tags, and describe your models thoroughly. Your API will be self-documenting, easy to consume, and a joy to work with.

Start documenting your APIs the FastAPI way - let your code generate the docs, and focus on building great features! 🚀

What's Next?

Continue your API development journey:

OpenAPI in Other Frameworks:

API Fundamentals:

Continue with FastAPI:

Resources

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.