Production engineering guide for api versioning strategies covering patterns, implementation strategies, and operational best practices.
API versioning is how systems evolve APIs over time while maintaining backward compatibility. It matters when clients depend on API contracts, and changes — adding fields, modifying behavior, deprecating endpoints — must be rolled out without breaking existing clients. The goal is to allow independent evolution of client and server, especially in distributed, polyglot environments.
Use path segments to distinguish versions. Clients request resources from /v1/users or /v2/orders, and the server routes based on the prefix.
GET /v1/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
The server matches the /v1/ prefix and dispatches to the v1 implementation. A new version can coexist with the old:
GET /v2/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
When a client sends Accept: application/vnd.example.com.v1+json, the server may return application/vnd.example.com.v2+json for the same endpoint. This allows the server to determine the version from the Accept header and route accordingly.
Clients must update their base URLs when upgrading. A common failure occurs when clients hardcode the version in the URL but forget to update the Accept header. The server returns v2 data, but the client expects v1 schema — a mismatch that silently breaks deserialization.
Versioned paths require a routing layer or middleware to dispatch requests. In a Node.js service, a simple middleware might look like:
const versionMiddleware = (req, res, next) => {
const version = req.path.match(/^\/v(\d+)(\/|$)/)?.[1];
if (!version) {
return res.status(400).json({
error: "Missing API version",
message: "API version must be in path, e.g., /v1/users"
});
}
req.apiVersion = version;
next();
};
app.use("/v1", versionMiddleware, v1Routes);
app.use("/v2", versionMiddleware, v2Routes);
The Accept header is often used to support content negotiation for version-specific media types. A client can request application/vnd.example.com.v1+json, and the server responds with data shaped specifically for v1, even when the path is /v2/users. This enables a hybrid model: path-based routing, with media-type-driven content selection.
Use query strings to specify the API version, often with ?version=1 or ?v=1.
GET /users/123?v=2 HTTP/1.1
Host: api.example.com
Accept: application/json
This strategy is easy to adopt and requires no server configuration. Clients can dynamically switch versions without updating base URLs. It also supports versioning in environments where routing is not centralized, such as microservices deployed behind a reverse proxy.
The server extracts the version from the query string:
const getVersionFromQuery = (req) => {
const version = req.query.version || req.query.v;
if (!version) return null;
const parsed = parseInt(version, 10);
return isNaN(parsed) ? null : parsed;
};
A common failure mode is version ambiguity: clients may use ?v=1 in some endpoints and ?version=1 in others, leading to inconsistent behavior. The server must normalize the query key — mapping v to version — and use the first found.
Clients can also include versioning in Accept headers, combining path and query:
GET /users/123?v=2 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.com.v2+json
The server uses the query v=2 for routing and the Accept header to determine the expected response structure. If the Accept header specifies v1, but the query is v=2, the response may be in v2 format, but the client expects v1 — a silent mismatch.
This strategy works well when versioning is managed at the edge, such as in an API gateway. A configuration for a gateway like Kong or AWS API Gateway might include:
{
"routes": [
{
"name": "users-v2",
"paths": ["/users"],
"request_parameters": {
"querystring.version": "true"
},
"plugins": [
{
"name": "request-transformer",
"config": {
"add": {
"api_version": "{{request.query.version}}"
}
}
}
]
}
]
}
Use custom headers to define the API version, commonly X-API-Version or Accept-Version.
GET /users/123 HTTP/1.1
Host: api.example.com
X-API-Version: 2
Accept: application/json
The server reads the header and selects the appropriate version implementation. This is especially useful when clients are built with SDKs or when the version is determined at the gateway layer.
Clients can dynamically switch versions by setting the header:
const client = {
async fetchUsers(id, version) {
const response = await fetch(`/users/${id}`, {
headers: {
'X-API-Version': version,
'Accept': 'application/json'
}
});
return await response.json();
}
};
A common failure occurs when clients send X-API-Version but forget to set Accept to a versioned media type. The server returns data in application/json, but the client expects application/vnd.example.com.v2+json. This mismatch can cause deserialization errors or missing fields.
The server must extract and validate the header:
const extractApiVersion = (req) => {
const header = req.headers.get('X-API-Version');
if (!header) return null;
const version = parseInt(header, 10);
if (isNaN(version)) {
return null;
}
return version;
};
Clients can also use Accept-Version for a more standardized approach:
Accept-Version: 2
This header allows multiple versions to be specified, such as Accept-Version: 2, 3, enabling backward compatibility and negotiation.
Use the Content-Type and Accept headers to encode the version in the media type string.
GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.com.v2+json
The server parses the Accept header and selects the appropriate version. The format application/vnd.example.com.v1+json allows versioning through the +json suffix, and the v2 in the type name.
Clients send Accept: application/vnd.example.com.v1+json and expect data structured for v1. The server returns data in the correct shape, and the client deserializes accordingly.
The server must match the media type against available versions:
const matchMediaType = (acceptHeader, versionMap) => {
const types = acceptHeader.split(',').map(t => t.trim());
for (const type of types) {
const match = type.match(/^application\/vnd\.example\.com\.v(\d+)\+json$/);
if (match) {
const version = parseInt(match[1], 10);
if (versionMap[version]) {
return version;
}
}
}
return null;
};
A frequent mistake is sending application/vnd.example.com.v1+json but receiving application/json data. This happens when the server returns Content-Type: application/json but does not include the version in the Content-Type. Clients expect versioned data, but the response is unversioned.
To fix this, the server must set the correct Content-Type:
res.setHeader('Content-Type', 'application/vnd.example.com.v2+json');
Clients can also negotiate multiple versions:
Accept: application/vnd.example.com.v2+json, application/vnd.example.com.v1+json
The server chooses the best match and returns data in the preferred version.
Use the + suffix in media types to encode version information, a pattern used in REST APIs.
GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.com+json
The server returns data with Content-Type: application/vnd.example.com.v2+json and supports multiple versions. Clients can request application/vnd.example.com+json and the server selects the latest version.
This strategy enables version discovery. A client can request application/vnd.example.com+json and the server returns the most up-to-date data format. The client can then cache the response and use it for future requests.
The server may include a Link header to indicate available versions:
Link: </v1/users>; rel="version"; type="application/vnd.example.com.v1+json",
</v2/users>; rel="version"; type="application/vnd.example.com.v2+json"
Clients can discover available versions and choose the appropriate one. This is especially useful in client SDKs or when building API explorers.
Use schema versions to manage backward compatibility. Clients may include a Schema-Version header or embed version metadata in the response.
GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.com.v2+json
The server returns:
{
"data": {
"id": 123,
"name": "Alice",
"created_at": "2023-01-01T12:00:00Z"
},
"schema_version": "2.1",
"meta": {
"api_version": "2",
"timestamp": "2023-01-01T12:00:00Z"
}
}
Clients can validate that the received data matches the expected schema version. A client built for schema_version: 2.0 may fail when receiving 2.1 with new fields.
A common failure is schema drift: the server evolves the schema, but clients do not update their expectations. The server adds a last_login field in v2.1, but clients expect only id, name, and created_at. The client deserializes the response, but last_login is ignored.
To mitigate this, clients can use schema versioning in the Accept header:
Accept: application/vnd.example.com.v2.1+json
The server returns data shaped for v2.1, and the client can handle new fields gracefully.
When multiple strategies are combined, clients must handle version selection carefully.
A client may send:
GET /users/123?v=2 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.com.v1+json
X-API-Version: 2
The server must resolve the version from:
v=2X-API-Version: 2application/vnd.example.com.v1+jsonThe server resolves the version as follows:
Accept header: v1 → use v1 schemaX-API-Version: 2 → use v2 implementationv: 2 → use v2 routingThe final version is determined by a priority order: Accept > X-API-Version > query, with Accept taking precedence.
The server returns:
HTTP/1.1 200 OK
Content-Type: application/vnd.example.com.v2+json
X-API-Version: 2
Schema-Version: 2.1
The client now knows the actual version used and can adapt its behavior.
This pattern enables robust, long-lived API contracts. Clients can evolve independently, and servers can deploy changes without breaking existing clients. The key to success is consistency: versioning must be applied uniformly across endpoints, headers, and responses.
This page was rewritten on 17 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