FastAPI Documentation Best Practices
Introduction
Documentation is crucial for any API project. With FastAPI, you get automatic interactive documentation out of the box, but knowing how to leverage this feature and enhance it with proper annotations can make your API much more usable and maintainable. This guide covers best practices for documenting your FastAPI applications effectively.
In this tutorial, you'll learn:
- How to write clear docstrings and annotations
- How to organize endpoint documentation
- How to customize Swagger UI and ReDoc
- How to implement proper examples and markdown in your documentation
The Importance of Good API Documentation
Good documentation helps:
- New developers understand your API quickly
- Users integrate with your API efficiently
- Future maintainers (including yourself) understand the code months later
- Reduce support requests and confusion
FastAPI generates documentation automatically from your Python code, but following these best practices will make your documentation truly shine.
Essential FastAPI Documentation Features
Basic Path Operation Documentation
Adding descriptions to your endpoints is the first step to good documentation:
@app.get(
"/items/",
summary="Get a list of items",
description="Retrieve a list of all items with optional filtering by status",
response_description="List of available items"
)
async def get_items():
"""
Retrieve all items from the database.
The items can be filtered by passing query parameters.
"""
return {"items": [{"id": 1, "name": "Hammer"}]}
This produces documentation with:
- A concise summary
- A detailed description
- A description of the response
Adding Detailed Docstrings
FastAPI uses your function docstrings as extended descriptions:
@app.post("/items/")
async def create_item(item: Item):
"""
Create a new item.
This endpoint allows you to:
- Add a new item to the database
- Validate the item data
- Return the created item with an ID
The item must have a name and can optionally include a description and price.
"""
return {"id": 1, **item.dict()}
Adding Response Examples
Response examples make your API easier to understand:
@app.get(
"/users/{user_id}",
responses={
200: {
"description": "Successful response",
"content": {
"application/json": {
"example": {
"id": 123,
"name": "Jane Doe",
"email": "[email protected]",
"active": True
}
}
}
},
404: {
"description": "User not found",
"content": {
"application/json": {
"example": {"detail": "User with ID 123 not found"}
}
}
}
}
)
async def get_user(user_id: int):
"""Get user details by ID."""
# Implementation goes here
pass
Using Tags for Organization
Tags help organize your API documentation into logical groups:
@app.post("/users/", tags=["Users"])
async def create_user(user: User):
"""Create a new user."""
return {"id": 1, **user.dict()}
@app.get("/items/", tags=["Items"])
async def get_items():
"""Get all items."""
return {"items": []}
You can also add tag metadata:
app = FastAPI(
title="My API",
openapi_tags=[
{
"name": "Users",
"description": "Operations related to user management",
},
{
"name": "Items",
"description": "Item management endpoints",
},
]
)
Documenting Pydantic Models
Properly documented data models are key to good API documentation:
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(
...,
title="Item name",
description="The name of the item, must be unique",
min_length=1,
max_length=50,
example="Screwdriver"
)
description: str = Field(
None,
title="Item description",
description="Optional detailed description of the item",
max_length=1000,
example="A Phillips head screwdriver with a rubber grip handle"
)
price: float = Field(
...,
title="Item price",
description="The price of the item in USD",
gt=0,
example=9.99
)
is_offer: bool = Field(
False,
title="Offer status",
description="Whether the item is currently on offer/sale"
)
class Config:
schema_extra = {
"example": {
"name": "Hammer",
"description": "A claw hammer with a wooden handle",
"price": 12.50,
"is_offer": True
}
}
Adding Parameter Details
Document query parameters, path parameters, and body parameters:
from fastapi import Path, Query, Body
@app.get("/items/{item_id}")
async def read_item(
item_id: int = Path(
...,
title="Item ID",
description="The ID of the item to retrieve",
ge=1
),
q: str = Query(
None,
title="Search Query",
description="Optional search query string to filter results",
min_length=3,
max_length=50
)
):
"""
Retrieve a specific item by its ID.
You can also filter by providing a search query parameter.
"""
return {"item_id": item_id, "q": q}
Using Response Models
Define what your API returns with response models:
from typing import List
class ItemResponse(BaseModel):
id: int
name: str
description: str = None
price: float
class Config:
schema_extra = {
"example": {
"id": 42,
"name": "Screwdriver",
"description": "A Phillips head screwdriver",
"price": 9.99
}
}
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int):
"""Get a specific item by ID."""
return {
"id": item_id,
"name": "Example Item",
"price": 19.99
}
@app.get("/items/", response_model=List[ItemResponse])
async def get_items():
"""Get all items."""
return [
{"id": 1, "name": "Item 1", "price": 19.99},
{"id": 2, "name": "Item 2", "price": 29.99}
]
Adding Markdown to Descriptions
Use Markdown to format your documentation:
@app.get("/markdown-docs/")
async def get_markdown_docs():
"""
# API Documentation with Markdown
You can use **bold text**, *italic text*, and other Markdown features:
## Features
- Lists
- Tables
- Code blocks
def example_code():
return {"message": "This is an example"}
For more information, visit [FastAPI's documentation](https://fastapi.tiangolo.com/).
"""
return {"message": "Check out the docs"}
Customizing Swagger UI and ReDoc
You can customize the API documentation UI:
from fastapi import FastAPI
app = FastAPI(
title="My Super API",
description="This is a **FastAPI** application with _awesome_ documentation.",
version="1.0.0",
docs_url="/documentation", # Change Swagger UI URL
redoc_url="/redocs", # Change ReDoc URL
openapi_url="/api/v1/openapi.json", # Change OpenAPI URL
swagger_ui_parameters={
"defaultModelsExpandDepth": -1, # Hide schemas section by default
"syntaxHighlight.theme": "monokai" # Change syntax highlighting theme
}
)
Including External Documentation
Link to external documentation:
@app.get(
"/external-docs/",
summary="Endpoint with external docs",
description="This endpoint has a reference to external documentation",
openapi_extra={
"externalDocs": {
"description": "Find more information here",
"url": "https://example.com/more-docs"
}
}
)
async def external_docs():
return {"message": "Check the external docs"}
Handling Deprecation
Document deprecated endpoints:
@app.get(
"/old-endpoint/",
tags=["Deprecated"],
deprecated=True,
summary="This endpoint is deprecated"
)
async def old_endpoint():
"""
This endpoint is deprecated and will be removed in future versions.
Please use `/new-endpoint/` instead.
"""
return {"message": "This is a deprecated endpoint"}
Real-World Example: Blog API Documentation
Let's put everything together in a real-world example:
from fastapi import FastAPI, Path, Query, HTTPException, status
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
app = FastAPI(
title="Blog API",
description="A simple blog API with comprehensive documentation",
version="1.0.0",
openapi_tags=[
{
"name": "Posts",
"description": "Operations related to blog posts",
},
{
"name": "Comments",
"description": "Operations related to post comments",
},
{
"name": "Users",
"description": "User management operations",
},
]
)
class PostBase(BaseModel):
title: str = Field(..., title="Post Title", description="Title of the blog post", min_length=1, max_length=100)
content: str = Field(..., title="Post Content", description="Main content of the blog post", min_length=10)
class PostCreate(PostBase):
tags: List[str] = Field(default=[], title="Tags", description="List of tags associated with the post")
class Config:
schema_extra = {
"example": {
"title": "FastAPI Documentation Guide",
"content": "This is a guide about creating great documentation with FastAPI...",
"tags": ["fastapi", "documentation", "tutorial"]
}
}
class PostResponse(PostBase):
id: int = Field(..., title="Post ID", description="Unique identifier for the post")
created_at: datetime = Field(..., title="Creation Date", description="When the post was created")
updated_at: Optional[datetime] = Field(None, title="Last Update", description="When the post was last updated")
tags: List[str] = Field(default=[], title="Tags", description="List of tags associated with the post")
class Config:
schema_extra = {
"example": {
"id": 1,
"title": "FastAPI Documentation Guide",
"content": "This is a guide about creating great documentation with FastAPI...",
"created_at": "2023-04-01T12:00:00Z",
"updated_at": "2023-04-02T15:30:00Z",
"tags": ["fastapi", "documentation", "tutorial"]
}
}
@app.post(
"/posts/",
response_model=PostResponse,
status_code=status.HTTP_201_CREATED,
tags=["Posts"],
summary="Create a new blog post",
response_description="The created blog post"
)
async def create_post(post: PostCreate):
"""
Create a new blog post with the provided information.
## Request Body
- **title**: Title of the blog post (1-100 characters)
- **content**: Main content of the blog post (min 10 characters)
- **tags**: Optional list of tag strings for categorization
## Returns
The created blog post with generated ID and timestamps
"""
# In a real app, you'd save to a database here
return {
"id": 1,
"created_at": datetime.now(),
"updated_at": None,
**post.dict()
}
@app.get(
"/posts/",
response_model=List[PostResponse],
tags=["Posts"],
summary="List all blog posts",
response_description="List of blog posts"
)
async def list_posts(
skip: int = Query(0, title="Skip", description="Number of posts to skip", ge=0),
limit: int = Query(10, title="Limit", description="Maximum number of posts to return", ge=1, le=100),
tag: Optional[str] = Query(None, title="Tag Filter", description="Filter posts by tag")
):
"""
Retrieve a list of blog posts with pagination support.
## Query Parameters
- **skip**: Number of records to skip for pagination (default: 0)
- **limit**: Maximum number of records to return (default: 10, max: 100)
- **tag**: Optional tag to filter posts by
## Returns
A list of blog posts, possibly filtered by tag
"""
# In a real app, you'd query a database here
posts = [
{
"id": 1,
"title": "FastAPI Documentation Guide",
"content": "This is a guide about creating great documentation with FastAPI...",
"created_at": datetime.now(),
"updated_at": None,
"tags": ["fastapi", "documentation", "tutorial"]
},
{
"id": 2,
"title": "Python Tips and Tricks",
"content": "Helpful Python techniques every developer should know...",
"created_at": datetime.now(),
"updated_at": None,
"tags": ["python", "programming"]
}
]
# Apply tag filtering if specified
if tag:
posts = [post for post in posts if tag in post["tags"]]
# Apply pagination
return posts[skip:skip+limit]
@app.get(
"/posts/{post_id}",
response_model=PostResponse,
tags=["Posts"],
summary="Get a specific blog post",
responses={
200: {
"description": "Successful response",
"content": {
"application/json": {
"example": {
"id": 1,
"title": "FastAPI Documentation Guide",
"content": "This is a guide about creating great documentation with FastAPI...",
"created_at": "2023-04-01T12:00:00Z",
"updated_at": "2023-04-02T15:30:00Z",
"tags": ["fastapi", "documentation", "tutorial"]
}
}
}
},
404: {
"description": "Post not found",
"content": {
"application/json": {
"example": {"detail": "Post with ID 123 not found"}
}
}
}
}
)
async def get_post(
post_id: int = Path(..., title="Post ID", description="The ID of the post to retrieve", ge=1)
):
"""
Retrieve a specific blog post by its ID.
## Path Parameters
- **post_id**: The unique identifier of the post to retrieve
## Returns
The requested blog post details
## Raises
- **404**: If the post with the specified ID is not found
"""
# In a real app, you'd query a database here
if post_id != 1:
raise HTTPException(status_code=404, detail=f"Post with ID {post_id} not found")
return {
"id": post_id,
"title": "FastAPI Documentation Guide",
"content": "This is a guide about creating great documentation with FastAPI...",
"created_at": datetime.now(),
"updated_at": None,
"tags": ["fastapi", "documentation", "tutorial"]
}
Best Practices Summary
-
Be Consistent: Use consistent naming, formatting, and structure throughout your documentation.
-
Use Meaningful Names: Choose clear, descriptive names for parameters, models, and endpoints.
-
Document Everything: Path operations, parameters, request bodies, responses, and exceptions.
-
Use Response Models: Define exactly what your API returns for better type hints and documentation.
-
Organize with Tags: Group related endpoints together with descriptive tags.
-
Add Examples: Provide realistic examples for requests and responses.
-
Use Status Codes Correctly: Document the appropriate HTTP status codes for each endpoint.
-
Keep Documentation Updated: Ensure documentation stays in sync with code changes.
-
Use Markdown: Format descriptions with markdown for better readability.
-
Document Error Responses: Explain what can go wrong and how errors are reported.
Summary
FastAPI makes it easy to create great documentation, but it's up to you to provide the right context and details. By following these best practices, you can create documentation that is helpful, comprehensive, and maintainable.
Remember that good documentation is a key aspect of API design. Invest time in writing clear explanations and examples, and your API users (including your future self) will thank you.
Additional Resources
Exercises
-
Take an existing FastAPI application and improve its documentation using the best practices from this guide.
-
Create a new FastAPI endpoint that demonstrates at least five different documentation features.
-
Compare the documentation generated by FastAPI with other API documentation systems. What are the advantages and limitations?
-
Create a model with complex validation rules and document it thoroughly with examples and clear descriptions.
-
Write a script that checks if all endpoints in your FastAPI application have proper documentation.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)