Skip to main content
โšก Calmops

API Versioning Strategies: Evolution Without Breaking Changes

Introduction

APIs are contracts between services and clients. As your application evolves, you need to make changes without breaking existing integrations. API versioning provides a way to introduce changes while maintaining backward compatibility.

This article explores different versioning strategies, when to use each, implementation patterns, and best practices for API evolution.

Why API Versioning Matters

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   API Versioning Challenges                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚  Changes that require versioning:                               โ”‚
โ”‚  - Removing or renaming fields                                  โ”‚
โ”‚  - Changing field types                                         โ”‚
โ”‚  - Changing response structure                                  โ”‚
โ”‚  - Adding required fields                                        โ”‚
โ”‚  - Removing endpoints                                            โ”‚
โ”‚  - Changing authentication requirements                         โ”‚
โ”‚                                                                 โ”‚
โ”‚  Without versioning:                                            โ”‚
โ”‚                                                                 โ”‚
โ”‚  Client v1         Server updates           Client v1           โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€          โ”‚
โ”‚     โ”‚              โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚   Deploy     โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚              โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚   Response   โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚   changed!   โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚              โ”‚                        โ”‚                   โ”‚
โ”‚     โ”‚              โ”‚                        โ”‚   ๐Ÿ”ด BROKEN!      โ”‚
โ”‚                                                                 โ”‚
โ”‚  With versioning:                                               โ”‚
โ”‚                                                                 โ”‚
โ”‚  Client v1 โ”€โ”€โ–ถ /api/v1/users  โ”€โ”€โ–ถ  Server maintains v1          โ”‚
โ”‚  Client v2 โ”€โ”€โ–ถ /api/v2/users  โ”€โ”€โ–ถ  Server returns v2           โ”‚
โ”‚                                                                 โ”‚
โ”‚  Both clients work! โœ…                                          โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Versioning Strategies

1. URI Path Versioning

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  URI Path Versioning                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚  GET /api/v1/users              GET /api/v2/users               โ”‚
โ”‚  GET /api/v1/users/123          GET /api/v2/users/123           โ”‚
โ”‚  POST /api/v1/users            POST /api/v2/users               โ”‚
โ”‚                                                                 โ”‚
โ”‚  Pros:                                                          โ”‚
โ”‚  - Simple to implement                                          โ”‚
โ”‚  - Easy to route and manage                                     โ”‚
โ”‚  - Visible in URL                                               โ”‚
โ”‚  - Easy to cache                                                โ”‚
โ”‚                                                                 โ”‚
โ”‚  Cons:                                                          โ”‚
โ”‚  - URL pollution                                                 โ”‚
โ”‚  - Duplicate code for similar endpoints                         โ”‚
โ”‚  - Changes affect all clients                                   โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
# FastAPI URI versioning
from fastapi import FastAPI, APIRouter, HTTPException
from typing import Optional
from datetime import datetime
from pydantic import BaseModel

app = FastAPI()

# Version routers
v1_router = APIRouter(prefix="/api/v1")
v2_router = APIRouter(prefix="/api/v2")

# Models
class UserV1(BaseModel):
    id: int
    name: str
    email: str
    created_at: str

class UserV2(BaseModel):
    id: int
    name: str
    email: str
    profile: Optional[dict] = None
    status: str = "active"
    created_at: str
    updated_at: str

# Mock database
users_db = {
    1: {"id": 1, "name": "John", "email": "[email protected]", "created_at": "2024-01-01"},
    2: {"id": 2, "name": "Jane", "email": "[email protected]", "created_at": "2024-01-02"},
}

# v1 Endpoints
@v1_router.get("/users", response_model=list[UserV1])
async def get_users_v1():
    return list(users_db.values())

@v1_router.get("/users/{user_id}", response_model=UserV1)
async def get_user_v1(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    return users_db[user_id]

@v1_router.post("/users", response_model=UserV1)
async def create_user_v1(user: UserV1):
    new_id = max(users_db.keys()) + 1
    user_data = user.dict()
    user_data["id"] = new_id
    users_db[new_id] = user_data
    return user_data

# v2 Endpoints
@v2_router.get("/users", response_model=list[UserV2])
async def get_users_v2(status: Optional[str] = None):
    users = []
    for user in users_db.values():
        user_data = {
            **user,
            "profile": {"bio": "User bio", "avatar": f"/avatars/{user['id']}.jpg"},
            "status": "active",
            "updated_at": datetime.utcnow().isoformat(),
        }
        if status and user_data["status"] != status:
            continue
        users.append(user_data)
    return users

@v2_router.get("/users/{user_id}", response_model=UserV2)
async def get_user_v2(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    
    user = users_db[user_id]
    return {
        **user,
        "profile": {"bio": "User bio", "avatar": f"/avatars/{user['id']}.jpg"},
        "status": "active",
        "updated_at": datetime.utcnow().isoformat(),
    }

@v2_router.post("/users", response_model=UserV2)
async def create_user_v2(user: UserV2):
    new_id = max(users_db.keys()) + 1
    now = datetime.utcnow().isoformat()
    user_data = {
        "id": new_id,
        "name": user.name,
        "email": user.email,
        "created_at": now,
        "profile": user.profile or {},
        "status": user.status,
        "updated_at": now,
    }
    users_db[new_id] = user_data
    return user_data

# Register routers
app.include_router(v1_router)
app.include_router(v2_router)

2. Header-Based Versioning

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                Header-Based Versioning                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚  GET /api/users  headers:                                        โ”‚
โ”‚    Accept: application/json                                     โ”‚
โ”‚    Accept-Version: v1                                           โ”‚
โ”‚                                                                 โ”‚
โ”‚  GET /api/users  headers:                                        โ”‚
โ”‚    Accept: application/json                                     โ”‚
โ”‚    Accept-Version: v2                                           โ”‚
โ”‚                                                                 โ”‚
โ”‚  Alternative:                                                   โ”‚
โ”‚    X-API-Version: v1                                            โ”‚
โ”‚                                                                 โ”‚
โ”‚  Pros:                                                          โ”‚
โ”‚  - Clean URLs                                                   โ”‚
โ”‚  - Can version individual resources                             โ”‚
โ”‚  - Flexible                                                     โ”‚
โ”‚                                                                 โ”‚
โ”‚  Cons:                                                          โ”‚
โ”‚  - Less visible                                                 โ”‚
โ”‚  - Harder to test                                               โ”‚
โ”‚  - Cache complexity                                              โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
from fastapi import FastAPI, Header, HTTPException
from typing import Optional

app = FastAPI()

async def get_api_version(x_api_version: Optional[str] = Header(None)):
    """Extract API version from header."""
    return x_api_version or "v1"

@app.get("/users")
async def get_users(
    x_api_version: Optional[str] = Header(None, alias="X-API-Version"),
):
    """Get users with header-based versioning."""
    version = x_api_version or "v1"
    
    if version == "v1":
        return await get_users_v1()
    elif version == "v2":
        return await get_users_v2()
    else:
        raise HTTPException(
            status_code=400,
            detail=f"Unsupported API version: {version}"
        )

@app.get("/users/{user_id}")
async def get_user(
    user_id: int,
    x_api_version: Optional[str] = Header(None),
):
    """Get user by ID with header-based versioning."""
    version = x_api_version or "v1"
    
    if version == "v1":
        return await get_user_v1(user_id)
    elif version == "v2":
        return await get_user_v2(user_id)
    else:
        raise HTTPException(
            status_code=400,
            detail=f"Unsupported API version: {version}"
        )

3. Query Parameter Versioning

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              Query Parameter Versioning                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚  GET /api/users?version=1                                       โ”‚
โ”‚  GET /api/users?version=2                                       โ”‚
โ”‚  GET /api/users?api_version=v2                                  โ”‚
โ”‚                                                                 โ”‚
โ”‚  Pros:                                                          โ”‚
โ”‚  - Easy to implement                                            โ”‚
โ”‚  - Clients can choose version easily                            โ”‚
โ”‚  - Can be optional (default to latest)                          โ”‚
โ”‚                                                                 โ”‚
โ”‚  Cons:                                                          โ”‚
โ”‚  - Can lead to complex URLs                                     โ”‚
โ”‚  - Not RESTful (version is state, not resource)                โ”‚
โ”‚  - Caching issues                                               โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
@app.get("/users")
async def get_users(
    version: Optional[str] = Query("v1", regex="^(v1|v2)$"),
):
    """Get users with query parameter versioning."""
    if version == "v1":
        return await get_users_v1()
    elif version == "v2":
        return await get_users_v2()

4. Content Negotiation (Media Type)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚             Content Negotiation Versioning                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚  GET /api/users                                                 โ”‚
โ”‚  Accept: application/vnd.myapp.v1+json                         โ”‚
โ”‚                                                                 โ”‚
โ”‚  GET /api/users                                                 โ”‚
โ”‚  Accept: application/vnd.myapp.v2+json                         โ”‚
โ”‚                                                                 โ”‚
โ”‚  Response:                                                     โ”‚
โ”‚  Content-Type: application/vnd.myapp.v2+json                   โ”‚
โ”‚                                                                 โ”‚
โ”‚  Pros:                                                          โ”‚
โ”‚  - True RESTful approach                                        โ”‚
โ”‚  - Standard HTTP content negotiation                            โ”‚
โ”‚  - Version per resource                                          โ”‚
โ”‚                                                                 โ”‚
โ”‚  Cons:                                                          โ”‚
โ”‚  - Complex to implement                                          โ”‚
โ”‚  - Harder for clients                                            โ”‚
โ”‚  - Less visible                                                  โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
from fastapi import FastAPI
from fastapi.responses import JSONResponse

@app.get("/users", responses={
    200: {
        "content": {
            "application/vnd.myapp.v1+json": {"schema": UserV1Schema},
            "application/vnd.myapp.v2+json": {"schema": UserV2Schema},
        }
    }
})
async def get_users(accept: str = Header(None)):
    """Get users with content negotiation."""
    
    # Parse Accept header
    version = "v1"
    if accept:
        if "v2" in accept:
            version = "v2"
    
    if version == "v1":
        return JSONResponse(
            content=await get_users_v1(),
            media_type="application/vnd.myapp.v1+json"
        )
    else:
        return JSONResponse(
            content=await get_users_v2(),
            media_type="application/vnd.myapp.v2+json"
        )

Versioning Best Practices

Version Strategy Comparison

Strategy Visibility RESTful Caching Complexity
URI Path High Medium Easy Low
Header Low High Medium Medium
Query Param Medium Low Medium Low
Media Type Low High Medium High
class VersionedAPIRouter:
    """Centralized version management."""
    
    VERSIONS = {
        "v1": {
            "deprecated": True,
            "sunset_date": "2026-06-01",
            "deprecation_notice": "v1 will be removed",
        },
        "v2": {
            "deprecated": False,
            "sunset_date": None,
        }
    }
    
    def __init__(self, default_version: str = "v2"):
        self.default_version = default_version
        self.routers = {}
    
    def register_version(self, version: str, router):
        """Register a versioned router."""
        self.routers[version] = router
    
    async def route_request(self, request):
        """Route request to appropriate version."""
        # Extract version from URL path
        path = request.url.path
        
        version = self.default_version
        for v in self.VERSIONS:
            if f"/api/{v}/" in path:
                version = v
                break
        
        # Check if version exists
        if version not in self.routers:
            raise HTTPException(
                status_code=404,
                detail=f"API version {version} not found"
            )
        
        # Check deprecation
        version_info = self.VERSIONS.get(version, {})
        if version_info.get("deprecated"):
            request.state.deprecation_warning = version_info.get(
                "deprecation_notice", 
                f"API version {version} is deprecated"
            )
        
        return await self.routers[version].dispatch(request)

Deprecation Strategy

Sunset Headers

from fastapi import Response

@app.middleware("http")
async def add_deprecation_headers(request, call_next):
    """Add deprecation headers to responses."""
    response = await call_next(request)
    
    # Check if version is deprecated
    version = extract_version(request.url.path)
    version_info = VersionedAPIRouter.VERSIONS.get(version, {})
    
    if version_info.get("deprecated"):
        response.headers["Deprecation"] = "true"
        
        if version_info.get("sunset_date"):
            response.headers["Sunset"] = version_info["sunset_date"]
        
        if version_info.get("deprecation_notice"):
            response.headers["Link"] = f'<{version_info.get("docs_url")}>; rel="deprecation"'
    
    return response

Deprecation Response

{
    "warning": "API version v1 is deprecated",
    "deprecated_at": "2026-01-01",
    "sunset_date": "2026-06-01",
    "message": "Please migrate to v2. Documentation: https://api.example.com/docs/v2",
    "migration_guide": "https://api.example.com/docs/migration/v1-to-v2"
}

Versioning with API Gateway

# API Gateway configuration (Kong)
services:
  - name: user-service
    url: http://user-service:8000
    routes:
      - name: users-v1
        paths:
          - /api/v1/users
        strip_path: false
        plugins:
          - name: rate-limiting
            config:
              minute: 100
          - name: request-transformer
            config:
              add:
                headers:
                  - "X-API-Version:v1"
      - name: users-v2
        paths:
          - /api/v2/users
        strip_path: false
        plugins:
          - name: rate-limiting
            config:
              minute: 1000
          - name: request-transformer
            config:
              add:
                headers:
                  - "X-API-Version:v2"
# AWS API Gateway
openapi: 3.0.0
info:
  title: User API

servers:
  - url: https://api.example.com/v1
    description: Version 1 (deprecated)
  - url: https://api.example.com/v2
    description: Version 2 (current)

paths:
  /users:
    get:
      summary: Get users
      operationId: getUsers
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Users'

Client Version Handling

// API Client with automatic versioning
class APIClient {
    constructor(baseUrl, defaultVersion = 'v2') {
        this.baseUrl = baseUrl;
        this.defaultVersion = defaultVersion;
        this.version = defaultVersion;
    }
    
    setVersion(version) {
        this.version = version;
    }
    
    async request(endpoint, options = {}) {
        const url = `${this.baseUrl}/api/${this.version}${endpoint}`;
        
        const response = await fetch(url, {
            ...options,
            headers: {
                'Accept': `application/vnd.myapp.${this.version}+json`,
                ...options.headers,
            }
        });
        
        // Handle deprecation warnings
        if (response.headers.get('Deprecation')) {
            console.warn(
                `API version ${this.version} is deprecated. ` +
                `Sunset date: ${response.headers.get('Sunset')}`
            );
        }
        
        return response.json();
    }
    
    // Convenience methods
    async getUsers() {
        return this.request('/users');
    }
    
    async getUser(id) {
        return this.request(`/users/${id}`);
    }
}

// Usage
const api = new APIClient('https://api.example.com');

// Use specific version
const v1Users = await api.setVersion('v1').getUsers();
const v2Users = await api.setVersion('v2').getUsers();

Migration Strategies

Gradual Migration

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 Gradual Migration Strategy                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚  Phase 1: Deploy v2                                             โ”‚
โ”‚  - Run v1 and v2 simultaneously                                โ”‚
โ”‚  - Monitor v2 usage                                             โ”‚
โ”‚  - 90% v1, 10% v2                                              โ”‚
โ”‚                                                                 โ”‚
โ”‚  Phase 2: Encourage migration                                  โ”‚
โ”‚  - Add deprecation warnings to v1 responses                    โ”‚
โ”‚  - Update documentation                                        โ”‚
โ”‚  - Notify major API consumers                                   โ”‚
โ”‚  - 60% v1, 40% v2                                               โ”‚
โ”‚                                                                 โ”‚
โ”‚  Phase 3: Deprecate v1                                          โ”‚
โ”‚  - Set sunset date                                              โ”‚
โ”‚  - Return deprecation headers                                  โ”‚
โ”‚  - 20% v1, 80% v2                                               โ”‚
โ”‚                                                                 โ”‚
โ”‚  Phase 4: Sunset v1                                             โ”‚
โ”‚  - Remove v1 after sunset date                                  โ”‚
โ”‚  - Keep v1 available for critical clients (if needed)          โ”‚
โ”‚  - 0% v1, 100% v2                                               โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
class VersionMigrationManager:
    """Manage API version migration."""
    
    def __init__(self):
        self.migrations = {
            "v1": {
                "status": "deprecated",
                "sunset_date": "2026-06-01",
                "migration_guide": "/docs/migration/v1-to-v2",
                "supported_until": "2026-06-01",
            }
        }
    
    def check_version_status(self, version: str) -> dict:
        """Check status of API version."""
        info = self.migrations.get(version, {})
        
        if not info:
            return {
                "valid": True,
                "status": "current"
            }
        
        return {
            "valid": True,
            "status": info.get("status", "current"),
            "sunset_date": info.get("sunset_date"),
            "migration_guide": info.get("migration_guide"),
        }
    
    def handle_deprecated_request(self, version: str) -> dict:
        """Handle requests to deprecated versions."""
        info = self.migrations.get(version, {})
        
        return {
            "warning": f"API version {version} is deprecated",
            "sunset_date": info.get("sunset_date"),
            "migration_guide": info.get("migration_guide"),
            "supported_until": info.get("supported_until"),
        }

Version Compatibility

Semantic Versioning for APIs

class APIVersion:
    """Semantic versioning for APIs."""
    
    def __init__(self, major: int, minor: int = 0, patch: int = 0):
        self.major = major
        self.minor = minor
        self.patch = patch
    
    def __str__(self):
        return f"v{self.major}.{self.minor}.{self.patch}"
    
    def is_compatible_with(self, other: 'APIVersion') -> bool:
        """Check if versions are compatible."""
        # Same major version = backward compatible
        return self.major == other.major
    
    def breaking_change_from(self, other: 'APIVersion') -> bool:
        """Check if this version has breaking changes."""
        return self.major > other.major


# Version compatibility matrix
COMPATIBILITY = {
    ("v1", "v1"): "compatible",
    ("v1", "v2"): "breaking",  # Major version change
    ("v2", "v2"): "compatible",
    ("v2", "v3"): "compatible",  # Minor version change
}

Conclusion

API versioning is essential for evolving your APIs without breaking existing clients. The choice of strategy depends on your specific needs: URI path versioning for simplicity, header-based for clean URLs, or content negotiation for true RESTful design.

Key takeaways:

  • Choose a versioning strategy early and document it
  • Implement deprecation headers and notices
  • Provide clear migration paths
  • Support multiple versions during transition periods
  • Monitor version usage and plan sunset carefully

Resources

Comments