API versioning is a critical aspect of modern API design that ensures backward compatibility while allowing for evolution and improvement of your API surface. As APIs mature and requirements change, versioning provides a structured approach to introducing breaking changes without disrupting existing consumers.
APIs evolve for various reasons: business requirements change, security vulnerabilities are discovered, or better design patterns emerge. Without proper versioning strategies, these changes can break existing integrations, leading to frustrated developers and potential revenue loss.
Breaking changes typically include:
This approach embeds the version directly in the URI path, making it explicit and easy to understand.
GET /api/v1/users/123
GET /api/v2/users/123
Implementation Example (Node.js/Express):
// v1/users.js
const express = require('express');
const router = express.Router();
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
name: 'John Doe',
email: 'john@example.com'
});
});
module.exports = router;
// v2/users.js
const express = require('express');
const router = express.Router();
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
profile: {
avatar: 'https://example.com/avatar.jpg',
lastLogin: '2025-01-15T10:30:00Z'
}
});
});
module.exports = router;
// app.js
const app = express();
app.use('/api/v1/users', require('./v1/users'));
app.use('/api/v2/users', require('./v2/users'));
This strategy uses query parameters to specify the API version.
GET /api/users/123?version=1
GET /api/users/123?api-version=2
Implementation Example (Python/FastAPI):
from fastapi import FastAPI, Query
from datetime import datetime
from typing import Optional
app = FastAPI()
# Version 1 response
class UserResponseV1:
def __init__(self, user_id: int, name: str, email: str):
self.id = user_id
self.name = name
self.email = email
# Version 2 response
class UserResponseV2:
def __init__(self, user_id: int, first_name: str, last_name: str,
email: str, profile: dict):
self.id = user_id
self.first_name = first_name
self.last_name = last_name
self.email = email
self.profile = profile
@app.get("/api/users/{user_id}")
async def get_user(
user_id: int,
api_version: Optional[str] = Query("1", alias="version")
):
if api_version == "1":
return UserResponseV1(
user_id=user_id,
name="John Doe",
email="john@example.com"
)
elif api_version == "2":
return UserResponseV2(
user_id=user_id,
first_name="John",
last_name="Doe",
email="john@example.com",
profile={
"avatar": "https://example.com/avatar.jpg",
"lastLogin": datetime.now().isoformat()
}
)
This approach uses custom HTTP headers to specify the API version, keeping the URI clean.
GET /api/users/123
Accept: application/vnd.company.user-v1+json
GET /api/users/123
Accept: application/vnd.company.user-v2+json
Implementation Example (C#/ASP.NET Core):
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new MediaTypeApiVersionReader("v");
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
// Controllers/UsersController.cs
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
[Produces("application/vnd.company.user-v1+json")]
public IActionResult GetUserV1(int id)
{
return Ok(new
{
Id = id,
Name = "John Doe",
Email = "john@example.com"
});
}
[HttpGet("{id}")]
[MapToApiVersion("2.0")]
[Produces("application/vnd.company.user-v2+json")]
public IActionResult GetUserV2(int id)
{
return Ok(new
{
Id = id,
FirstName = "John",
LastName = "Doe",
Email = "john@example.com",
Profile = new
{
Avatar = "https://example.com/avatar.jpg",
LastLogin = DateTime.UtcNow
}
});
}
}
Adopt semantic versioning (SemVer) principles for your API versions:
# OpenAPI specification example
openapi: 3.1.0
info:
title: User API
version: 2.1.0
description: User management API version 2.1.0
Implement clear deprecation policies and communicate them effectively to consumers.
GET /api/v1/users/123
Deprecation: Thu, 01 Jun 2025 00:00:00 GMT
Sunset: Thu, 01 Dec 2025 00:00:00 GMT
Link: </api/v2/users/123>; rel="successor-version"
Support multiple version negotiation strategies to accommodate different client capabilities.
// Version negotiation middleware
const versionNegotiation = (req, res, next) => {
// Check Accept header first
const acceptHeader = req.get('Accept');
if (acceptHeader && acceptHeader.includes('vnd.company.user-v2')) {
req.apiVersion = '2.0';
} else if (acceptHeader && acceptHeader.includes('vnd.company.user-v1')) {
req.apiVersion = '1.0';
}
// Fall back to query parameter
else if (req.query.version === '2') {
req.apiVersion = '2.0';
} else {
req.apiVersion = '1.0'; // Default version
}
next();
};
Use feature flags to enable new versions for specific clients or percentages of traffic.
# feature-flags.yaml
api:
v2:
enabled: true
rollout_percentage: 25
allowed_clients:
- "mobile-app-v2.5+"
- "web-dashboard-v3.0+"
Leverage API gateways for intelligent version routing and traffic management.
# API Gateway configuration
routes:
- path: /api/users
methods: [GET]
routing_rules:
- condition: header["x-api-version"] == "2"
target: user-service-v2
- condition: query["version"] == "2"
target: user-service-v2
- default: user-service-v1
Implement thorough testing strategies for all supported versions.
# test_api_versions.py
import pytest
from fastapi.testclient import TestClient
def test_user_api_v1(client: TestClient):
response = client.get("/api/users/123?version=1")
assert response.status_code == 200
data = response.json()
assert "name" in data
assert "firstName" not in data
def test_user_api_v2(client: TestClient):
response = client.get(
"/api/users/123",
headers={"Accept": "application/vnd.company.user-v2+json"}
)
assert response.status_code == 200
data = response.json()
assert "firstName" in data
assert "lastName" in data
assert "profile" in data
Validate requests and responses against version-specific schemas.
// JSON Schema validation for different versions
const userSchemas = {
'1.0': {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string' }
},
required: ['id', 'name', 'email']
},
'2.0': {
type: 'object',
properties: {
id: { type: 'integer' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
profile: {
type: 'object',
properties: {
avatar: { type: 'string' },
lastLogin: { type: 'string' }
}
}
},
required: ['id', 'firstName', 'lastName', 'email']
}
};
Track version adoption and usage patterns to inform deprecation timelines.
-- Analytics query for version usage
SELECT
api_version,
COUNT(*) as request_count,
AVG(response_time) as avg_response_time,
COUNT(CASE WHEN status_code >= 400 THEN 1 END) as error_count
FROM api_requests
WHERE endpoint = '/api/users'
AND timestamp >= NOW() - INTERVAL 30 DAY
GROUP BY api_version
ORDER BY request_count DESC;
As you evolve your API, prioritize backward compatibility and minimize breaking changes. When breaking changes are necessary, provide ample migration time and clear upgrade paths. Remember that successful API versioning is as much about communication and developer experience as it is about technical implementation.