API versioning strategies including URL path, header, and content negotiation. Use when migrating v1 to v2, handling breaking changes, implementing deprecation or sunset policies, or managing backward compatibility.
Scanned 2/12/2026
Install via CLI
openskills install yonatangross/orchestkit---
name: api-versioning
description: API versioning strategies including URL path, header, and content negotiation. Use when migrating v1 to v2, handling breaking changes, implementing deprecation or sunset policies, or managing backward compatibility.
context: fork
agent: backend-system-architect
version: 1.0.0
tags: [api, versioning, rest, fastapi, backward-compatibility]
author: OrchestKit
user-invocable: false
---
# API Versioning Strategies
Design APIs that evolve gracefully without breaking clients.
## Strategy Comparison
| Strategy | Example | Pros | Cons |
|----------|---------|------|------|
| URL Path | `/api/v1/users` | Simple, visible, cacheable | URL pollution |
| Header | `X-API-Version: 1` | Clean URLs | Hidden, harder to test |
| Query Param | `?version=1` | Easy testing | Messy, cache issues |
| Content-Type | `Accept: application/vnd.api.v1+json` | RESTful | Complex |
## URL Path Versioning (Recommended)
### FastAPI Structure
```
backend/app/
├── api/
│ ├── v1/
│ │ ├── __init__.py
│ │ ├── routes/
│ │ │ ├── users.py
│ │ │ └── analyses.py
│ │ └── router.py
│ ├── v2/
│ │ ├── __init__.py
│ │ ├── routes/
│ │ │ ├── users.py # Updated schemas
│ │ │ └── analyses.py
│ │ └── router.py
│ └── router.py # Combines all versions
```
### Router Setup
```python
# backend/app/api/router.py
from fastapi import APIRouter
from app.api.v1.router import router as v1_router
from app.api.v2.router import router as v2_router
api_router = APIRouter()
api_router.include_router(v1_router, prefix="/v1")
api_router.include_router(v2_router, prefix="/v2")
# main.py
app.include_router(api_router, prefix="/api")
```
### Version-Specific Schemas
```python
# v1/schemas/user.py
class UserResponseV1(BaseModel):
id: str
name: str # Single name field
# v2/schemas/user.py
class UserResponseV2(BaseModel):
id: str
first_name: str # Split into first/last
last_name: str
full_name: str # Computed for convenience
```
### Shared Business Logic
```python
# services/user_service.py (version-agnostic)
class UserService:
async def get_user(self, user_id: str) -> User:
return await self.repo.get_by_id(user_id)
# v1/routes/users.py
@router.get("/{user_id}", response_model=UserResponseV1)
async def get_user_v1(user_id: str, service: UserService = Depends()):
user = await service.get_user(user_id)
return UserResponseV1(id=user.id, name=user.full_name)
# v2/routes/users.py
@router.get("/{user_id}", response_model=UserResponseV2)
async def get_user_v2(user_id: str, service: UserService = Depends()):
user = await service.get_user(user_id)
return UserResponseV2(
id=user.id,
first_name=user.first_name,
last_name=user.last_name,
full_name=f"{user.first_name} {user.last_name}",
)
```
## Header-Based Versioning
```python
from fastapi import Header, HTTPException
async def get_api_version(
x_api_version: str = Header(default="1", alias="X-API-Version")
) -> int:
try:
version = int(x_api_version)
if version not in [1, 2]:
raise ValueError()
return version
except ValueError:
raise HTTPException(400, "Invalid API version")
@router.get("/users/{user_id}")
async def get_user(
user_id: str,
version: int = Depends(get_api_version),
service: UserService = Depends(),
):
user = await service.get_user(user_id)
if version == 1:
return UserResponseV1(id=user.id, name=user.full_name)
else:
return UserResponseV2(
id=user.id,
first_name=user.first_name,
last_name=user.last_name,
)
```
## Content Negotiation
```python
from fastapi import Request
MEDIA_TYPES = {
"application/vnd.orchestkit.v1+json": 1,
"application/vnd.orchestkit.v2+json": 2,
"application/json": 2, # Default to latest
}
async def get_version_from_accept(request: Request) -> int:
accept = request.headers.get("Accept", "application/json")
return MEDIA_TYPES.get(accept, 2)
@router.get("/users/{user_id}")
async def get_user(
user_id: str,
version: int = Depends(get_version_from_accept),
):
...
```
## Deprecation Headers
```python
from fastapi import Response
from datetime import date
def add_deprecation_headers(
response: Response,
deprecated_date: date,
sunset_date: date,
link: str,
):
response.headers["Deprecation"] = deprecated_date.isoformat()
response.headers["Sunset"] = sunset_date.isoformat()
response.headers["Link"] = f'<{link}>; rel="successor-version"'
# Usage in v1 endpoints
@router.get("/users/{user_id}")
async def get_user_v1(user_id: str, response: Response):
add_deprecation_headers(
response,
deprecated_date=date(2025, 1, 1),
sunset_date=date(2025, 7, 1),
link="https://api.example.com/v2/users",
)
return await service.get_user(user_id)
```
## Version Lifecycle
```
┌─────────────────────────────────────────────────────────────────┐
│ VERSION LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ ALPHA │ → │ BETA │ → │ STABLE │ → │ DEPRECATED │ │
│ │ (dev) │ │ (test) │ │ (prod) │ │ (sunset) │ │
│ └─────────┘ └─────────┘ └──────────┘ └─────────────┘ │
│ │
│ v3-alpha v3-beta v2 (current) v1 (6 months) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ POLICY: │
│ • Deprecation notice: 3 months before sunset │
│ • Sunset period: 6 months after deprecation │
│ • Support: Latest stable + 1 previous version │
└─────────────────────────────────────────────────────────────────┘
```
## Breaking vs Non-Breaking Changes
### Non-Breaking (No Version Bump)
```python
# Adding optional fields
class UserResponse(BaseModel):
id: str
name: str
avatar_url: str | None = None # New optional field
# Adding new endpoints
@router.get("/users/{user_id}/preferences") # New endpoint
# Adding optional query params
@router.get("/users")
async def list_users(
limit: int = 100,
cursor: str | None = None, # New pagination
):
```
### Breaking (Requires Version Bump)
```python
# Removing fields
# Renaming fields
# Changing field types
# Changing URL structure
# Changing authentication
# Removing endpoints
# Changing error formats
```
## OpenAPI Per Version
```python
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
def custom_openapi_v1():
return get_openapi(
title="OrchestKit API",
version="1.0.0",
routes=v1_router.routes,
)
def custom_openapi_v2():
return get_openapi(
title="OrchestKit API",
version="2.0.0",
routes=v2_router.routes,
)
app.mount("/docs/v1", create_docs_app(custom_openapi_v1))
app.mount("/docs/v2", create_docs_app(custom_openapi_v2))
```
## Anti-Patterns (FORBIDDEN)
```python
# NEVER version internal implementation
class UserServiceV1: # Services should be version-agnostic
...
# NEVER break contracts without versioning
class UserResponse(BaseModel):
# Changed from `name` to `full_name` without version bump!
full_name: str
# NEVER sunset without notice
# Just removing v1 routes one day
# NEVER support too many versions (max 2-3)
/api/v1/... # Ancient
/api/v2/...
/api/v3/...
/api/v4/... # Too many!
```
## Key Decisions
| Decision | Recommendation |
|----------|----------------|
| Strategy | URL path (`/api/v1/`) |
| Support window | Current + 1 previous |
| Deprecation notice | 3 months minimum |
| Sunset period | 6 months after deprecation |
| Breaking changes | New major version |
| Additive changes | Same version (backward compatible) |
## Related Skills
- `api-design-framework` - REST API patterns
- `error-handling-rfc9457` - Consistent errors across versions
- `observability-monitoring` - Version usage metrics
## Capability Details
### url-versioning
**Keywords:** url version, path version, /v1/, /v2/
**Solves:**
- How to version REST APIs?
- URL-based API versioning
### header-versioning
**Keywords:** header version, X-API-Version, custom header
**Solves:**
- Clean URL versioning
- Header-based API version
### deprecation
**Keywords:** deprecation, sunset, version lifecycle, backward compatible
**Solves:**
- How to deprecate API versions?
- Version sunset policy
### breaking-changes
**Keywords:** breaking change, non-breaking, backward compatible
**Solves:**
- What requires a version bump?
- Breaking vs non-breaking changes
No comments yet. Be the first to comment!