FastAPI Version Management
Introduction
As your API grows and evolves, you'll need to make changes to endpoints, response schemas, and business logic. But what happens when clients are still relying on the previous behavior? This is where API versioning comes in.
API versioning allows you to evolve your API while maintaining backward compatibility for existing clients. In this guide, we'll explore different approaches to version management in FastAPI applications, helping you implement a versioning strategy that works for your project.
Why Version Your API?
Before diving into implementation details, let's understand why API versioning is important:
- Backward Compatibility: Allows existing clients to continue functioning without disruption
- Parallel Development: Enables you to work on new features without affecting production
- Incremental Migration: Gives clients time to adopt new API versions
- Documentation: Makes it clear which features are available in each version
API Versioning Approaches in FastAPI
FastAPI provides several ways to implement versioning. Let's explore the most common approaches:
1. URL Path Versioning
This is the most straightforward approach where the version is included directly in the URL path.
from fastapi import FastAPI
app = FastAPI(title="My Versioned API")
@app.get("/v1/users")
async def get_users_v1():
return {"version": "v1", "data": ["user1", "user2"]}
@app.get("/v2/users")
async def get_users_v2():
return {
"version": "v2",
"data": [
{"id": 1, "username": "user1"},
{"id": 2, "username": "user2"}
]
}
Accessing /v1/users
would return:
{
"version": "v1",
"data": ["user1", "user2"]
}
While /v2/users
would return:
{
"version": "v2",
"data": [
{"id": 1, "username": "user1"},
{"id": 2, "username": "user2"}
]
}
Advantages:
- Simple to implement and understand
- Clearly visible in documentation
- Works well with API clients and browsers
2. APIRouter with Prefixes
For larger applications, using FastAPI's APIRouter
with version prefixes helps organize your code better:
from fastapi import FastAPI, APIRouter
app = FastAPI(title="My Versioned API")
# Create routers for each version
router_v1 = APIRouter(prefix="/v1")
router_v2 = APIRouter(prefix="/v2")
# V1 endpoints
@router_v1.get("/users")
async def get_users_v1():
return {"version": "v1", "data": ["user1", "user2"]}
# V2 endpoints
@router_v2.get("/users")
async def get_users_v2():
return {
"version": "v2",
"data": [
{"id": 1, "username": "user1"},
{"id": 2, "username": "user2"}
]
}
# Include routers in the main app
app.include_router(router_v1)
app.include_router(router_v2)
This approach helps keep your code organized by grouping endpoints by version.
3. Header-Based Versioning
Some APIs prefer to use HTTP headers for versioning:
from fastapi import FastAPI, Header, HTTPException
from typing import Optional
app = FastAPI(title="My Versioned API")
@app.get("/users")
async def get_users(api_version: Optional[str] = Header(None)):
if api_version == "1.0":
return {"version": "1.0", "data": ["user1", "user2"]}
elif api_version == "2.0":
return {
"version": "2.0",
"data": [
{"id": 1, "username": "user1"},
{"id": 2, "username": "user2"}
]
}
else:
# Default to latest version or return an error
raise HTTPException(status_code=400, detail="Invalid API version")
Clients would make requests with the api_version
header:
GET /users
api_version: 1.0
Advantages:
- Keeps the URL clean
- Follows the HTTP specification for content negotiation
- Allows for more flexible versioning schemes
4. Query Parameter Versioning
Another approach is to use query parameters:
from fastapi import FastAPI, Query, HTTPException
app = FastAPI(title="My Versioned API")
@app.get("/users")
async def get_users(version: str = Query("1.0")):
if version == "1.0":
return {"version": "1.0", "data": ["user1", "user2"]}
elif version == "2.0":
return {
"version": "2.0",
"data": [
{"id": 1, "username": "user1"},
{"id": 2, "username": "user2"}
]
}
else:
raise HTTPException(status_code=400, detail="Unsupported version")
Clients would access:
GET /users?version=1.0
Managing Versioned Pydantic Models
As your API evolves, your data models will likely change too. Here's how you can manage versioned models:
from fastapi import FastAPI, APIRouter
from pydantic import BaseModel
app = FastAPI(title="My Versioned API")
# V1 Models
class UserV1(BaseModel):
name: str
# V2 Models
class UserV2(BaseModel):
id: int
username: str
email: str
active: bool = True
# Create routers
router_v1 = APIRouter(prefix="/v1")
router_v2 = APIRouter(prefix="/v2")
# V1 endpoints
@router_v1.post("/users")
async def create_user_v1(user: UserV1):
return {"version": "v1", "user": user.dict()}
# V2 endpoints
@router_v2.post("/users")
async def create_user_v2(user: UserV2):
return {"version": "v2", "user": user.dict()}
# Include routers
app.include_router(router_v1)
app.include_router(router_v2)
This approach ensures proper validation and documentation for each version of your API.
Best Practices for API Versioning
- Choose a Consistent Approach: Pick one versioning method and stick with it across your API
- Version Only When Necessary: Don't create new versions for minor changes that don't break compatibility
- Support Multiple Versions: Allow a grace period for clients to migrate
- Document Changes: Clearly document what has changed between versions
- Default to Latest Version: For clients that don't specify a version, default to the most recent stable version
- Deprecate Old Versions: Set clear timelines for when old versions will be retired
Implementation Example: Version Control with Dependency Injection
For more complex applications, you can use FastAPI's dependency injection system to control versioning:
from fastapi import FastAPI, Depends, HTTPException, Header
from typing import Callable, Optional
app = FastAPI(title="Advanced Versioned API")
# Version-specific logic
def get_user_service(version: Optional[str] = Header(None)):
if version == "1.0":
return UserServiceV1()
elif version == "2.0":
return UserServiceV2()
else:
# Default to latest
return UserServiceV2()
class UserServiceV1:
def get_users(self):
return ["user1", "user2"]
class UserServiceV2:
def get_users(self):
return [{"id": 1, "username": "user1"}, {"id": 2, "username": "user2"}]
@app.get("/users")
async def get_users(service: Callable = Depends(get_user_service)):
return {"data": service.get_users()}
This approach allows you to swap out entire service implementations based on the requested version.
Managing Database Changes with Versioning
API versioning often coincides with database schema changes. Here's a pattern for handling this:
from fastapi import FastAPI, Depends, Header
from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session
from database import get_db
app = FastAPI()
# User retrieval with version-specific behavior
async def get_user_by_id(
user_id: int,
db: Session = Depends(get_db),
api_version: Optional[str] = Header(None)
):
# Query the database
user = db.query(User).filter(User.id == user_id).first()
if not user:
return None
# Format response according to version
if api_version == "1.0":
return {
"name": user.name,
"email": user.email
}
else: # Default to v2
return {
"id": user.id,
"username": user.name,
"email": user.email,
"active": user.active,
"created_at": user.created_at.isoformat()
}
@app.get("/users/{user_id}")
async def read_user(user_data: Dict[str, Any] = Depends(get_user_by_id)):
if user_data is None:
raise HTTPException(status_code=404, detail="User not found")
return user_data
This pattern allows you to use the same database models but present different views of the data based on the requested API version.
Summary
API versioning is essential for evolving your FastAPI applications while maintaining backward compatibility. In this guide, we've explored:
- Different versioning approaches (URL path, headers, query parameters)
- How to organize code using APIRouters
- Managing versioned Pydantic models
- Implementing versioning with dependency injection
- Best practices for API versioning
The approach you choose should depend on your specific requirements, team preferences, and the nature of your API. URL path versioning is often the simplest to implement and understand, making it a good default choice for many projects.
Additional Resources
- FastAPI Official Documentation
- REST API Versioning - Microsoft API Guidelines
- Semantic Versioning - For understanding version numbering
Exercises
- Implement a small FastAPI app with two versions of the same endpoint, using URL path versioning.
- Extend the example to use APIRouters with separate module files for each version.
- Create a versioning system that combines header-based versioning with dependency injection.
- Design a strategy for deprecating an old API version, including warning headers and documentation.
- Build a test suite that verifies both versions of your API work correctly.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)