home / skills / yonatangross / orchestkit / api-versioning

api-versioning skill

/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-versioning

Review the files below or copy the command above to add this skill to your agents.

Files (5)
SKILL.md
9.7 KB
---
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

Overview

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.

How this skill works

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 to use it

  • Migrating from v1 to v2 when introducing breaking changes (schema or URL changes).
  • Implementing deprecation or sunset policies to phase out older versions safely.
  • Supporting backwards compatibility while adding new fields or endpoints.
  • When you need clear, cacheable, and discoverable versioning for public APIs.
  • When teams want separate OpenAPI docs and docs mounts per API version.

Best practices

  • Prefer URL path versioning (/api/v1/) for visibility, caching friendliness, and simple routing.
  • Keep business logic and services version-agnostic; translate models at the edge (route layer).
  • Treat additive changes as non-breaking and avoid version bumps for optional fields or new endpoints.
  • Define a clear lifecycle: deprecation notice ≥3 months, sunset period ≈6 months, support current +1 prior.
  • Publish per-version OpenAPI docs and include Deprecation/Sunset/Link headers on deprecated endpoints.

Example use cases

  • Expose /api/v1/users and /api/v2/users with different response schemas while using a single UserService.
  • Use an X-API-Version header to route internal clients without changing URLs during gradual rollout.
  • Default Accept-based content negotiation to latest version while allowing clients to pin older formats.
  • Add deprecation headers to v1 responses linking to v2 routes and announcing sunset dates.
  • Document v1 and v2 separately under /docs/v1 and /docs/v2 for clear client migration guidance.

FAQ

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.