home / skills / yonatangross / orchestkit / api-versioning
/plugins/ork/skills/api-versioning
This skill helps you design and manage API versioning strategies to migrate safely, deprecate endpoints, and maintain backward compatibility.
npx playbooks add skill yonatangross/orchestkit --skill api-versioningReview the files below or copy the command above to add this skill to your agents.
---
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, 2026]
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
This skill presents practical API versioning strategies and operational policies to evolve APIs without breaking clients. It compares URL path, header, query, and content-negotiation approaches, and recommends URL path versioning as the default. It also covers shared business logic patterns, OpenAPI per-version docs, deprecation headers, and a version lifecycle policy.
The skill inspects common versioning patterns and maps them to concrete implementation recipes (router layout, dependency-based header parsing, and Accept-header negotiation). It shows how to keep business logic version-agnostic while exposing version-specific schemas and routes. It includes deprecation header helpers, lifecycle timelines, and OpenAPI generation per version so teams can publish and document multiple API versions concurrently.
When should I bump the API version?
Bump the major version for breaking changes: removing or renaming fields, altering types, changing URL or auth, or changing error formats.
How many versions should we support concurrently?
Limit support to the latest stable and one previous version (max 2–3). More versions increase complexity and maintenance cost.