Tags: api, rest, versioning, backend, architecture, http
Level: intermediate
TL;DR: API versioning isolates breaking changes across consumer lifecycles using URI path, query parameters, custom headers, or content negotiation.
URI path versioning embeds major versions directly inside the resource route (/v1/users).
Header versioning preserves clean URIs by passing version identifiers via custom HTTP headers (X-API-Version).
Content negotiation uses the Accept header to request specific representation versions via media types.
Query parameter versioning specifies the version via query string parameters (/users?v=2).
Semantic Versioning (SemVer) guides changes: Major for breaking changes, Minor for additive features, Patch for fixes.
Breaking changes include removing fields, renaming endpoints, changing types, or adding required request parameters.
Non-breaking changes include adding optional query parameters, adding optional request fields, or returning new response fields.
Deprecation policies communicate upcoming sunsets via standard HTTP headers like Deprecation and Sunset.
Backward compatibility reduces operational overhead by minimizing the frequency of new major releases.
Routing layers or API gateways can direct versioned traffic to distinct downstream microservices.
Q: Which API versioning strategy do you prefer and why?R: I prefer URI path versioning for public APIs because it is explicit, easy to explore, and cache-friendly. For strict RESTful internal architectures, I lean toward custom request headers or Accept media types to maintain clean resource identifiers.
Q: How do you handle breaking changes without disrupting existing clients?R: I introduce a new major version while keeping the old version active in parallel. I flag the old endpoint with Sunset and Deprecation headers, publish a migration guide, and monitor access logs to confirm adoption before retiring it.
Q: Is adding a new field to an API response considered a breaking change?R: Typically no, provided clients adhere to the robustness principle and ignore unknown fields. However, it can break poorly implemented clients that enforce rigid schema validation or strict deserialization rules.
Incrementing major versions for non-breaking, additive field additions.
Versioning individual sub-resources independently instead of applying consistent service-level contracts.
Neglecting cache keys when using header-based versioning without setting the Vary header properly.
Failing to define an explicit deprecation timeline, leading to indefinite maintenance of obsolete endpoints.
Breaking database-level constraints when multiple API versions mutate the same underlying database schema concurrently.
// URI Path routing with Express
import express, { Request, Response } from 'express';
const app = express();
app.get('/api/v1/users', (req: Request, res: Response) => {
res.json([{ id: 1, name: 'Alice' }]);
});
app.get('/api/v2/users', (req: Request, res: Response) => {
res.json([{ id: 1, firstName: 'Alice', lastName: 'Smith' }]);
});
HTTP
# Content Negotiation versioning via Accept header
GET /api/users HTTP/1.1
Host: api.example.com
Accept: application/vnd.company.v2+json
# Deprecation and Sunset headers on legacy response
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 11 Nov 2026 00:00:00 GMT
Link: <https://api.example.com/docs/v2-migration>; rel="successor-version"
[ ] Handling cache invalidation strategies using the HTTP Vary header.
[ ] Contract testing with tools like Pact to catch unintentional breaking contract changes.
[ ] Database dual-write patterns during cross-version schema migrations.
[x] Standard REST URI path and query parameter routing structures.