Version APIs without breaking consumers. Covers URI versioning, header versioning, content negotiation, deprecation workflows, backward compatibility patterns, and the trade-offs that determine the right versioning strategy.
API versioning enables systems to evolve independently while maintaining backward compatibility. It is essential when multiple clients—mobile apps, web frontends, third-party integrations—rely on a shared backend service and changes to the API must not break existing clients. Use versioning when introducing new fields, changing data structures, deprecating endpoints, or modifying behaviors across releases.
When a client makes an API call, the version is communicated through the request. The most common methods are query parameters, headers, and URL paths.
Version is encoded in the URL path. Clients use /v1/ or /api/v2/ as the base path for a versioned API.
GET /v1/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
The server routes requests based on the first path segment after the hostname. A request to /v2/users/123 is handled by the v2 implementation, which may support additional fields in the user response.
This pattern is simple, human-readable, and easy to debug. It is widely supported in frameworks like Express.js, Django, Spring Boot, and Rails.
Version is passed as a query parameter, usually ?version=1 or ?v=1.
GET /users/123?v=2 HTTP/1.1
Host: api.example.com
Accept: application/json
The server extracts the v or version parameter from the query string and selects the appropriate handler. This method is lightweight and ideal for APIs consumed by JavaScript clients, especially SPAs.
Common pitfalls:
?v=1&version=2 where version takes precedence.Version is carried in an HTTP header, typically Accept-Version or API-Version.
GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
Accept-Version: 2
The server reads the Accept-Version header and selects the corresponding version handler. This approach is clean and ideal for microservices.
Key failure mode:
curl or fetch without explicit configuration.curl -H "Accept-Version: 2" https://api.example.com/users/123
The server must validate the header value: Accept-Version: 2 is not equivalent to Accept-Version: "2" or Accept-Version: 2.0. A version string is matched against a list of supported versions, with a fallback to the default version when no match is found.
The API response can include version metadata, helping clients understand the shape and behavior of the data they receive.
The server includes a X-API-Version header in the response.
HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: 2
Content-Length: 234
{
"id": 123,
"name": "Alice",
"created_at": "2023-06-15T12:00:00Z",
"preferences": {
"theme": "dark",
"notifications": true
}
}
Clients can log or monitor version drift across services. This is especially useful in distributed systems where different services may be at different versions.
The response body includes a top-level version field.
{
"version": "2",
"data": {
"id": 123,
"name": "Alice",
"created_at": "2023-06-15T12:00:00Z"
},
"links": {
"self": "/users/123",
"next": "/users/123?cursor=abc"
}
}
This pattern allows clients to dynamically adapt their logic based on the version, such as rendering different UI elements or applying different validation rules.
When the data model changes, the API must handle both old and new formats. This requires careful design of schemas and response transformations.
A new field is added in version 2, but clients using version 1 do not expect it.
// v1: response
{
"id": 123,
"name": "Alice"
}
// v2: response
{
"id": 123,
"name": "Alice",
"email": "[email protected]",
"created_at": "2023-06-15T12:00:00Z"
}
Clients using v1 can ignore the new email and created_at fields. The server must ensure that the v1 client receives a valid JSON document even when it sends ?v=1 to a v2 endpoint.
Critical point:
name to be a string but receives an object.When introducing a new field, the server provides default values for older clients.
{
"id": 123,
"name": "Alice",
"email": "[email protected]",
"preferences": {
"theme": "light",
"notifications": true
}
}
If the preferences.theme field is introduced in version 2, the server populates it with light as the default when serving version 1.
Clients using version 1 receive theme: "light" even though it was not defined in the v1 schema.
Error responses must communicate version-specific information.
The error response includes version-specific fields and codes.
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 123 was not found",
"details": {
"user_id": 123,
"version": "2"
},
"timestamp": "2023-06-15T12:05:30Z"
}
}
The code field maps to a specific error type, and the version field indicates which API version generated the error.
Clients can map error codes to user-facing messages, and use the version field to trigger version-specific recovery logic.
Different versions may use different error codes.
// v1: error response
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found"
}
}
// v2: error response
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 123 not found",
"details": {
"user_id": 123,
"suggestions": [121, 122, 124]
}
}
}
Clients can detect version mismatches by inspecting the error object. A v1 client receiving a details field in the error must handle it gracefully, possibly falling back to a generic message.
Versioning is not just about requests and responses—it spans the entire service lifecycle.
The service runs multiple versions simultaneously. A deployment strategy like blue-green or rolling updates is used.
$ ./deploy.sh --version=2 --strategy=blue-green
# Traffic is gradually shifted from v1 (blue) to v2 (green)
During the transition, both versions serve requests. Clients using ?v=1 or Accept-Version: 1 are routed to the v1 service, while new clients use ?v=2.
The API gateway routes requests to the appropriate versioned service.
# Example routing configuration
routes:
- path: /v1/users/*
service: users-v1
version: 1
- path: /v2/users/*
service: users-v2
version: 2
- path: /users/*
service: users-default
version: 1
Requests to /users/123 without a version are routed to the default version (v1), while /v2/users/123 is handled by the v2 service.
When a version is deprecated, the server sends deprecation headers.
HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: 2
Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: </v1/users>; rel="previous", </v3/users>; rel="next"
{
"id": 123,
"name": "Alice"
}
Clients can monitor the Sunset header to plan upgrades. The Link header includes URLs for the previous and next versions.
Common failure:
Sunset header but rely solely on the X-API-Version header.Link header is not parsed correctly: rel="previous" is not the same as rel="prev".Clients must handle versioning gracefully.
Clients detect the API version by inspecting the response.
const response = await fetch('/users/123', {
headers: { 'Accept-Version': '1' }
});
const version = response.headers.get('X-API-Version');
const body = await response.json();
if (version === '2') {
// Use v2-specific logic
renderUserWithPreferences(body.data);
} else {
// Fall back to v1
renderUserWithoutPreferences(body.data);
}
A client expects version 2 but receives version 1.
{
"error": {
"code": "VERSION_MISMATCH",
"message": "Client expects version 2, but received version 1",
"expected": 2,
"actual": 1,
"suggested": "/v2/users/123"
}
}
The client can redirect or retry the request with ?v=2 or send a Accept-Version: 2 header.
The server reads versioning configuration from a file.
# config.yml
api:
versioning:
strategy: header
default: 1
supported:
- 1
- 2
header: Accept-Version
query_param: v
path_prefix: v
deprecation:
- version: 1
sunset: "2024-06-30"
message: "Version 1 will be retired"
- version: 2
sunset: "2025-12-31"
message: "Version 2 is stable; version 1 is deprecated"
The server reads this configuration at startup and applies it to all routes.
When versioning fails silently, clients may break without clear indication.
A client sends Accept-Version: 2 but the server responds with version 1 data.
{
"id": 123,
"name": "Alice",
"email": "[email protected]"
}
The client expects created_at and preferences fields, but they are missing. No error is raised.
Root cause:
Accept-Version header.X-API-Version header.The versioning strategy is configured differently across environments.
Accept-Version header, default version 1?v=2, path-based, default version 1?v=2, header, and path, with version 2 as defaultClients deployed to different environments behave differently, leading to inconsistent behavior.
The server logs version information with every request.
{
"timestamp": "2023-06-15T12:00:00Z",
"method": "GET",
"path": "/users/123",
"version": "2",
"client_version": "1.2.0",
"request_id": "abc123",
"response_size": 234
}
These logs help diagnose version-specific issues and track adoption.
| Pattern | When to Use | Common Pitfalls |
|---|---|---|
| Path-based | Public APIs, REST clients | Version not included in request, routing errors |
| Query parameter | JavaScript clients, SPAs | Multiple version parameters, caching issues |
| Header-based | Microservices, internal APIs | Header not sent, value parsing errors |
| Response version header | Monitoring, observability | Version not logged, not cached |
| Versioned response body | Clients that adapt to schema changes | Missing version field, deserialization errors |
| Deprecation with sunset | Long-term client stability | Clients ignore sunset headers |
| Versioned error responses | Client-driven error handling | Error codes not mapped, missing details |
Versioning is not a single decision—it is a set of interlocking patterns that ensure clients and services remain aligned across releases.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy