418 lines
10 KiB
Markdown
418 lines
10 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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 users
|
|
- `POST /api/v1/users/` - Create user
|
|
- `GET /api/v1/posts/` - Get posts
|
|
|
|
## Current Structure
|
|
|
|
### Version 1 (v1)
|
|
|
|
The current API version is in `src/app/api/v1/`:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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)
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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)
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# Both versions work simultaneously
|
|
# v1: GET /api/v1/users/ -> list[UserRead]
|
|
# v2: GET /api/v2/users/ -> PaginatedListResponse[UserReadV2]
|
|
```
|
|
|
|
### 2. Add Deprecation Warnings
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
1. Release v2 alongside v1
|
|
2. Add deprecation warnings to v1
|
|
3. Set sunset date for v1
|
|
4. Monitor v1 usage
|
|
5. 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](../database/migrations.md)** - Handle database schema changes
|
|
- **[Testing](../testing.md)** - Test multiple API versions
|
|
- **[Production](../production.md)** - Deploy versioned APIs
|
|
|
|
Proper versioning lets you evolve your API without breaking existing clients! |