api-versioning
4
总安装量
4
周安装量
#49792
全站排名
安装命令
npx skills add https://github.com/yonatangross/skillforge-claude-plugin --skill api-versioning
Agent 安装分布
claude-code
3
opencode
2
antigravity
2
windsurf
1
trae
1
Skill 文档
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
# 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
# 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
# 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
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
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
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)
# 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)
# Removing fields
# Renaming fields
# Changing field types
# Changing URL structure
# Changing authentication
# Removing endpoints
# Changing error formats
OpenAPI Per Version
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)
# 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 patternserror-handling-rfc9457– Consistent errors across versionsobservability-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