API Versioning¶
Learn how to version your APIs properly using the boilerplate's built-in versioning structure and best practices for maintaining backward compatibility.
Quick Start¶
The boilerplate is already set up for versioning with a v1
structure:
src/app/api/
├── dependencies.py # Shared across all versions
└── v1/ # Version 1 of your API
├── __init__.py # Router registration
├── users.py # User endpoints
├── posts.py # Post endpoints
└── ... # Other endpoints
Your endpoints are automatically available at /api/v1/...
:
GET /api/v1/users/
- Get usersPOST /api/v1/users/
- Create userGET /api/v1/posts/
- Get posts
Current Structure¶
Version 1 (v1)¶
The current API version is in src/app/api/v1/
:
# src/app/api/v1/__init__.py
from fastapi import APIRouter
from .users import router as users_router
from .posts import router as posts_router
from .login import router as login_router
# Main v1 router
api_router = APIRouter()
# Include all v1 endpoints
api_router.include_router(users_router)
api_router.include_router(posts_router)
api_router.include_router(login_router)
Main App Registration¶
In src/app/main.py
, v1 is registered:
from fastapi import FastAPI
from app.api.v1 import api_router as api_v1_router
app = FastAPI()
# Register v1 API
app.include_router(api_v1_router, prefix="/api/v1")
Adding Version 2¶
When you need to make breaking changes, create a new version:
Step 1: Create v2 Directory¶
src/app/api/
├── dependencies.py
├── v1/ # Keep v1 unchanged
│ ├── __init__.py
│ ├── users.py
│ └── ...
└── v2/ # New version
├── __init__.py
├── users.py # Updated user endpoints
└── ...
Step 2: Create v2 Router¶
# src/app/api/v2/__init__.py
from fastapi import APIRouter
from .users import router as users_router
# Import other v2 routers
# Main v2 router
api_router = APIRouter()
# Include v2 endpoints
api_router.include_router(users_router)
Step 3: Register v2 in Main App¶
# src/app/main.py
from fastapi import FastAPI
from app.api.v1 import api_router as api_v1_router
from app.api.v2 import api_router as api_v2_router
app = FastAPI()
# Register both versions
app.include_router(api_v1_router, prefix="/api/v1")
app.include_router(api_v2_router, prefix="/api/v2")
Version 2 Example¶
Here's how you might evolve the user endpoints in v2:
v1 User Endpoint¶
# src/app/api/v1/users.py
from app.schemas.user import UserRead, UserCreate
@router.get("/", response_model=list[UserRead])
async def get_users():
users = await crud_users.get_multi(db=db, schema_to_select=UserRead)
return users["data"]
@router.post("/", response_model=UserRead)
async def create_user(user_data: UserCreate):
return await crud_users.create(db=db, object=user_data)
v2 User Endpoint (with breaking changes)¶
# src/app/api/v2/users.py
from app.schemas.user import UserReadV2, UserCreateV2 # New schemas
from fastcrud.paginated import PaginatedListResponse
# Breaking change: Always return paginated response
@router.get("/", response_model=PaginatedListResponse[UserReadV2])
async def get_users(page: int = 1, items_per_page: int = 10):
users = await crud_users.get_multi(
db=db,
offset=(page - 1) * items_per_page,
limit=items_per_page,
schema_to_select=UserReadV2
)
return paginated_response(users, page, items_per_page)
# Breaking change: Require authentication
@router.post("/", response_model=UserReadV2)
async def create_user(
user_data: UserCreateV2,
current_user: Annotated[dict, Depends(get_current_user)] # Now required
):
return await crud_users.create(db=db, object=user_data)
Schema Versioning¶
Create separate schemas for different versions:
Version 1 Schema¶
# src/app/schemas/user.py (existing)
class UserRead(BaseModel):
id: int
name: str
username: str
email: str
profile_image_url: str
tier_id: int | None
class UserCreate(BaseModel):
name: str
username: str
email: str
password: str
Version 2 Schema (with changes)¶
# src/app/schemas/user_v2.py (new file)
from datetime import datetime
class UserReadV2(BaseModel):
id: int
name: str
username: str
email: str
avatar_url: str # Changed from profile_image_url
subscription_tier: str # Changed from tier_id to string
created_at: datetime # New field
is_verified: bool # New field
class UserCreateV2(BaseModel):
name: str
username: str
email: str
password: str
accept_terms: bool # New required field
Gradual Migration Strategy¶
1. Keep Both Versions Running¶
# Both versions work simultaneously
# v1: GET /api/v1/users/ -> list[UserRead]
# v2: GET /api/v2/users/ -> PaginatedListResponse[UserReadV2]
2. Add Deprecation Warnings¶
# src/app/api/v1/users.py
import warnings
from fastapi import HTTPException
@router.get("/", response_model=list[UserRead])
async def get_users(response: Response):
# Add deprecation header
response.headers["X-API-Deprecation"] = "v1 is deprecated. Use v2."
response.headers["X-API-Sunset"] = "2024-12-31" # When v1 will be removed
users = await crud_users.get_multi(db=db, schema_to_select=UserRead)
return users["data"]
3. Monitor Usage¶
Track which versions are being used:
# src/app/api/middleware.py
from fastapi import Request
import logging
logger = logging.getLogger(__name__)
async def version_tracking_middleware(request: Request, call_next):
if request.url.path.startswith("/api/v1/"):
logger.info(f"v1 usage: {request.method} {request.url.path}")
elif request.url.path.startswith("/api/v2/"):
logger.info(f"v2 usage: {request.method} {request.url.path}")
response = await call_next(request)
return response
Shared Code Between Versions¶
Keep common logic in shared modules:
Shared Dependencies¶
# src/app/api/dependencies.py - shared across all versions
async def get_current_user(...):
# Authentication logic used by all versions
pass
async def get_db():
# Database connection used by all versions
pass
Shared CRUD Operations¶
# The CRUD layer can be shared between versions
# Only the schemas and endpoints change
# v1 endpoint
@router.get("/", response_model=list[UserRead])
async def get_users_v1():
users = await crud_users.get_multi(schema_to_select=UserRead)
return users["data"]
# v2 endpoint
@router.get("/", response_model=PaginatedListResponse[UserReadV2])
async def get_users_v2():
users = await crud_users.get_multi(schema_to_select=UserReadV2)
return paginated_response(users, page, items_per_page)
Version Discovery¶
Let clients discover available versions:
# src/app/api/versions.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/versions")
async def get_api_versions():
return {
"available_versions": ["v1", "v2"],
"current_version": "v2",
"deprecated_versions": [],
"sunset_dates": {
"v1": "2024-12-31"
}
}
Register it in main.py:
# src/app/main.py
from app.api.versions import router as versions_router
app.include_router(versions_router, prefix="/api")
# Now available at GET /api/versions
Testing Multiple Versions¶
Test both versions to ensure compatibility:
# tests/test_api_versioning.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_v1_users(client: AsyncClient):
"""Test v1 returns simple list"""
response = await client.get("/api/v1/users/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list) # v1 returns list
@pytest.mark.asyncio
async def test_v2_users(client: AsyncClient):
"""Test v2 returns paginated response"""
response = await client.get("/api/v2/users/")
assert response.status_code == 200
data = response.json()
assert "data" in data # v2 returns paginated response
assert "total_count" in data
assert "page" in data
OpenAPI Documentation¶
Each version gets its own docs:
# src/app/main.py
from fastapi import FastAPI
# Create separate apps for documentation
v1_app = FastAPI(title="My API v1", version="1.0.0")
v2_app = FastAPI(title="My API v2", version="2.0.0")
# Register routes
v1_app.include_router(api_v1_router)
v2_app.include_router(api_v2_router)
# Mount as sub-applications
main_app = FastAPI()
main_app.mount("/api/v1", v1_app)
main_app.mount("/api/v2", v2_app)
Now you have separate documentation:
- /api/v1/docs
- v1 documentation
- /api/v2/docs
- v2 documentation
Best Practices¶
1. Semantic Versioning¶
- v1.0 → v1.1: New features (backward compatible)
- v1.1 → v2.0: Breaking changes (new version)
2. Clear Migration Path¶
# Document what changed in v2
"""
API v2 Changes:
- GET /users/ now returns paginated response instead of array
- POST /users/ now requires authentication
- UserRead.profile_image_url renamed to avatar_url
- UserRead.tier_id changed to subscription_tier (string)
- Added UserRead.created_at and is_verified fields
- UserCreate now requires accept_terms field
"""
3. Gradual Deprecation¶
- Release v2 alongside v1
- Add deprecation warnings to v1
- Set sunset date for v1
- Monitor v1 usage
- Remove v1 after sunset date
4. Consistent Patterns¶
Keep the same patterns across versions:
- Same URL structure:
/api/v{number}/resource
- Same HTTP methods and status codes
- Same authentication approach
- Same error response format
What's Next¶
Now that you understand API versioning:
- Database Migrations - Handle database schema changes
- Testing - Test multiple API versions
- Production - Deploy versioned APIs
Proper versioning lets you evolve your API without breaking existing clients!