Skip to content
C Codeloom
REST APIs

REST API Versioning Strategies

Compare URL, header, and content-type versioning for REST APIs. Learn when to bump versions and how to retire old ones without breaking clients.

·3 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Why versioning matters
  • URL vs header vs media-type versioning
  • When to bump a version
  • Deprecation timelines
  • Tips for keeping multiple versions sane

Prerequisites

  • Comfortable building REST endpoints

What and Why

APIs change. Clients do not always change at the same pace. Versioning gives you a contract that lets both sides evolve. The goal is simple: avoid breaking existing clients while still being able to ship new features.

Mental Model

Three popular versioning styles dominate REST.

  • URL versioning: /v1/users, /v2/users.
  • Header versioning: Accept-Version: 2 or a custom X-API-Version.
  • Media-type versioning: Accept: application/vnd.acme.v2+json.

All three work. The right pick depends on your audience, tooling, and operational comfort.

URL:         GET /v2/users/1
Header:      GET /users/1
           Accept-Version: 2
Media type:  GET /users/1
           Accept: application/vnd.acme.v2+json
Versioning placement

Hands-on Example

URL versioning in FastAPI.

from fastapi import FastAPI, APIRouter

app = FastAPI()

v1 = APIRouter(prefix="/v1")
v2 = APIRouter(prefix="/v2")

@v1.get("/users/{id}")
def get_user_v1(id: int):
    return {"id": id, "name": "Ada"}

@v2.get("/users/{id}")
def get_user_v2(id: int):
    # adds email field
    return {"id": id, "name": "Ada", "email": "ada@x.io"}

app.include_router(v1)
app.include_router(v2)

Header versioning with a dependency.

from fastapi import Header, HTTPException

def api_version(x_api_version: str = Header("1")):
    if x_api_version not in {"1", "2"}:
        raise HTTPException(400, "Unsupported version")
    return x_api_version

@app.get("/users/{id}")
def get_user(id: int, version: str = Depends(api_version)):
    if version == "2":
        return {"id": id, "name": "Ada", "email": "ada@x.io"}
    return {"id": id, "name": "Ada"}

When to bump a version. Use semver-like rules.

  • Additive changes (new optional field, new endpoint): no bump.
  • Renamed or removed fields, changed types, or new required input: bump major.
  • Behavior changes that break assumptions: bump major.

Most teams favor major-only versions in REST and never publish minor versions. New features arrive on the latest major.

Deprecate clearly. Return a Deprecation header and a Sunset header that tells clients when the version goes away.

HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://api.acme.com/v2/users/1>; rel="successor-version"

Common Pitfalls

  • Versioning too often. Every change becomes a maintenance burden. Aim for stability.
  • Forking the entire codebase per version. Use one code path with version-aware serializers.
  • Removing v1 without warning. Clients you never knew existed will break. Use a sunset window of 6 to 12 months.
  • Putting business logic in version-specific code. Versions should be a thin shell over a shared core.

Practical Tips

  • Default to URL versioning if you have many third-party clients. It is the most cache and tooling friendly.
  • Prefer header or media-type versioning when you control all clients and want clean URLs.
  • Use response shapes per version with a single transformation layer. The service stays single-source-of-truth.
  • Track usage of each version. Sunset is data-driven, not calendar-driven.
  • Document the deprecation policy publicly. Predictability earns trust.

Wrap-up

Pick a versioning style that matches your clients and stick with it. Bump only when truly necessary, communicate deprecations long before they hit, and design your code so the shared logic lives in one place. With those habits, you can evolve the API for years without forcing painful migrations on the people who depend on it.