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
Usermodel - 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/docsFeatures:
- 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/redocFeatures:
- 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.jsonUse 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 itemResult: 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 itemDeprecating 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_openapiHiding 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 itemPath 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_openapiGenerating 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-typescriptPython
# Generate Python client
openapi-generator-cli generate \
-i http://localhost:8000/openapi.json \
-g python \
-o ./client-pythonGo
# Generate Go client
openapi-generator-cli generate \
-i http://localhost:8000/openapi.json \
-g go \
-o ./client-goReal-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."""
pass2. 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):
pass4. 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(): pass5. 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):
passAdvanced: 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.
"""
passConclusion
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 Documentation with OpenAPI/Swagger in Spring Boot — OpenAPI with Java and SpringDoc
- Spring Boot REST API Best Practices — Production-ready API patterns
API Fundamentals:
- What is REST API? Complete Guide — REST principles and design patterns
- HTTP Protocol Complete Guide — Deep dive into HTTP
Continue with FastAPI:
- FastAPI Authentication & Security — Secure your FastAPI endpoints
- FastAPI Database Integration — Connect FastAPI to databases
Resources
- OpenAPI Specification: https://swagger.io/specification/
- FastAPI OpenAPI Docs: https://fastapi.tiangolo.com/advanced/extending-openapi/
- Swagger UI: https://swagger.io/tools/swagger-ui/
- ReDoc: https://github.com/Redocly/redoc
- OpenAPI Generator: https://openapi-generator.tech/
📬 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.