API Versioning Strategies — Best Practices and Implementation Guide
API Versioning is the practice of managing changes to an API over time by assigning version identifiers, enabling backward compatibility so existing clients continue working while new features are released.
What You'll Learn
You will learn four major API Versioning strategies URI versioning, header versioning, query parameter versioning, and content negotiation along with their trade-offs and implementation patterns.
Why API Versioning Matters
Without versioning, a single breaking change can crash every client using your API. Mobile apps users cannot update immediately, third-party integrations may not be maintained, and internal services run on different release cycles. Proper versioning protects clients from breaking changes and gives you freedom to evolve your API.
Real-World Use
DodaTech operates multiple API versions simultaneously. Doda Browser v1 uses URI versioning, DodaZIP update service uses header versioning for fine-grained control, and Durga Antivirus Pro threat reporting uses content negotiation to support both JSON and Protocol Buffers formats.
API Versioning Learning Path
flowchart LR
A[REST API Design] --> B[Why Version?]
B --> C[URI Versioning]
B --> D[Header Versioning]
B --> E[Query Parameter]
B --> F[Content Negotiation]
C --> G[Choose Strategy]
D --> G
E --> G
F --> G
B:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Prerequisites
Understand RESTful Api Design Best Practices and HTTP Protocol Basics. Familiarity with API Development Concepts helps. You should know how to make HTTP requests with different headers and understand the concept of breaking vs non-breaking changes.
When to Version Your API
Not every change requires a new version. Classify changes as breaking or non-breaking.
Non-Breaking Changes
These can be released without versioning:
- Adding a new endpoint
- Adding optional fields to a response
- Adding new query parameters
- Relaxing validation rules
- Increasing rate limits
- Adding new HTTP methods to existing resources
Breaking Changes
These require a new version:
- Removing an endpoint
- Removing or renaming fields
- Changing field types
- Making optional fields required
- Changing endpoint URLs
- Changing authentication requirements
- Changing error response format
Strategy 1: URI Versioning
The most common approach. The version is part of the URL path.
GET /api/v1/users
GET /api/v2/users
Implementation in Express.js
const express = require("express");
const app = express();
// v1 routes
app.get("/api/v1/users", (req, res) => {
res.json({
version: "1.0",
data: [
{ id: 1, name: "Alice", email: "alice"@example".com" }
]
});
});
// v2 routes (added role field)
app.get("/api/v2/users", (req, res) => {
res.json({
version: "2.0",
data: [
{ id: 1, name: "Alice", email: "alice"@example".com", role: "admin" }
]
});
});
Implementation in FastAPI
from fastapi import FastAPI, APIRouter
v1 = APIRouter(prefix="/api/v1")
v2 = APIRouter(prefix="/api/v2")
@v1.get("/users")
async def get_users_v1():
return {"version": "1.0", "data": [
{"id": 1, "name": "Alice"}
]}
@v2.get("/users")
async def get_users_v2():
return {"version": "2.0", "data": [
{"id": 1, "name": "Alice", "role": "admin"}
]}
app = FastAPI()
app.include_router(v1)
app.include_router(v2)
Pros: Simple, explicit, cache-friendly, easy to route. Cons: URL pollution, violates REST principle of a consistent URI for a resource.
Strategy 2: Header Versioning
The version is specified in a custom HTTP header.
GET /api/users
Accept: application/vnd.dodatech.v1+json
Implementation in Express.js
app.get("/api/users", (req, res) => {
const accept = req.headers["accept"];
if (accept.includes("vnd.dodatech.v1+json")) {
return res.json({ version: "1.0", data: usersV1 });
}
if (accept.includes("vnd.dodatech.v2+json")) {
return res.json({ version: "2.0", data: usersV2 });
}
// Default to latest version
return res.json({ version: "2.0", data: usersV2 });
});
Pros: Clean URLs, REST-friendly, supports content negotiation. Cons: Harder to test in browsers, requires header modification, LESS visible to caching layers.
Strategy 3: Query Parameter Versioning
The version is a query parameter.
GET /api/users?version=1
GET /api/users?version=2
Implementation in Express.js
app.get("/api/users", (req, res) => {
const version = req.query.version || "1";
if (version === "1") {
return res.json({ version: "1.0", data: usersV1 });
}
if (version === "2") {
return res.json({ version: "2.0", data: usersV2 });
}
return res.status(400).json({ error: "Unsupported version" });
});
Pros: Easy to test, simple to implement. Cons: Can be forgotten by clients, pollutes query namespace, not RESTful.
Strategy 4: Content Negotiation
Use the standard Accept header with custom media types.
GET /api/users
Accept: application/json; version=1
Implementation in Express.js
app.get("/API/users", (req, res) => {
const accept = req.headers["accept"] || "";
if (accept.includes("version=1")) {
return res.JSON({ version: "1.0", data: usersV1 });
}
if (accept.includes("version=2")) {
return res.JSON({ version: "2.0", data: usersV2 });
}
return res.JSON({ version: "2.0", data: usersV2 });
});
Pros: Uses standard HTTP mechanism, clean URLs, supports multiple formats. Cons: Complex to implement, LESS discoverable.
Version Lifecycle Management
Define a clear lifecycle for each version:
const VERSIONS = {
v1: { status: "deprecated", sunset: "2026-12-31" },
v2: { status: "active", sunset: null },
v3: { status: "beta", sunset: null }
};
app.get("/API/v1/users", (req, res) => {
res.set("Sunset", VERSIONS.v1.sunset);
res.set("Deprecation", "true");
res.JSON({ version: "1.0", data: usersV1 });
});
Send deprecation headers to inform clients:
Deprecation: true— indicates the version is deprecatedSunset: Sat, 31 Dec 2026 23:59:59 GMT— when the version will be removedLink: </api/v2/users>; rel="successor-version"— points to the replacement
Common Errors
Not versioning from the start — Releasing an API without versioning, then needing to add it later. All early clients break when you introduce versions. Always include versioning from the first release, even if only v1 exists.
Removing old versions too quickly — Deprecating and removing versions before clients can migrate. Give at least six months notice before removing a version. Use the
Sunsetheader to communicate timelines.Versioning the entire API instead of specific resources — A single change to one endpoint forces a full API version bump. Consider per-resource versioning for large APIs.
Using semantic versioning for APIs — Versions like v1.2.3 imply frequent changes. Use simple integer versions (v1, v2, v3) for APIs. Save semantic versioning for the API implementation, not the API contract.
No deprecation headers — Removing versions without warning clients. Always send
DeprecationandSunsetheaders on deprecated versions.Supporting too many versions — Maintaining five or more API versions simultaneously. Support a maximum of three versions current, previous, and beta.
Inconsistent versioning across endpoints — Some endpoints on v1, others on v2. All endpoints in a version should be available and consistent. Version the entire API surface together.
Practice Questions
- What types of changes require a new API version?
- What are the advantages of URI versioning over header versioning?
- How should you communicate deprecation to API clients?
- What is the recommended number of API versions to support simultaneously?
- How does content negotiation differ from custom header versioning?
Challenge
Design an API Versioning Strategy for a SaaS platform with 50,000 active API clients. The platform currently has v1 with 200 endpoints. You need to introduce v2 that changes 30 endpoints. Write a Migration plan that includes: the versioning Strategy you choose and why, deprecation header implementation, sunset timeline (12 months), client communication plan, and surge pricing for v1 after the sunset date.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro