API Versioning
Status: Complete
Category: Architecture
Default enforcement: Soft
Author: PushBackLog team
Tags
- Topic: architecture, api-design
- Skillset: backend
- Technology: generic
- Stage: planning, execution, review
Summary
API versioning is the practice of managing breaking changes to a public or shared API in a way that allows existing consumers to continue working while new consumers adopt the updated contract. Every shared API will eventually require a breaking change; having no versioning strategy means every such change forces all consumers to update simultaneously, which is operationally impractical and contractually unreliable. The strategy should be chosen before the first breaking change, not after.
Rationale
Breaking changes are inevitable
Requirements change. Data models evolve. Security vulnerabilities require field removal or type changes. Performance improvements require query contract changes. An API that has any consumers will eventually need a breaking change. The question is not whether this will happen but whether the team has a strategy for managing it without breaking existing consumers.
An unversioned API treats the first breaking change as a crisis — all integrated consumers must update, coordinate, and deploy simultaneously. With a versioning strategy, breaking changes are an ordinary, manageable event.
The wrong approach: breaking changes without notice
The most common API versioning anti-pattern is to make breaking changes to an unversioned endpoint and hope consumers update quickly. In a microservices system, this creates deployment order dependencies. For external APIs, it violates client trust. For mobile clients, it risks breaking versions of the app that cannot be force-updated.
Guidance
Versioning strategies
URI path versioning (most common)
GET /v1/orders/{id}
GET /v2/orders/{id}
Pros: Explicit, visible, easy to route at the infrastructure level, easy to test
Cons: Pollutes the URI with a technical concern; requires client URL changes on upgrades
Header versioning (Accept header)
GET /orders/{id}
Accept: application/vnd.pushbacklog.v2+json
Pros: URIs remain stable; follows REST purist conventions
Cons: Harder to test in a browser, less visible in logs, requires clients to set custom headers
Query parameter versioning
GET /orders/{id}?version=2
Pros: Easy to append to existing URLs
Cons: Semantically wrong — query parameters should filter resources, not select API behaviour; easy to omit accidentally
Header: API version key
GET /orders/{id}
X-API-Version: 2
Pros: Separates versioning from content negotiation
Cons: Non-standard; may be dropped by proxies
Recommendation: URI path versioning for most APIs. The explicitness outweighs the URI purity concern, and infrastructure routing is simpler.
What constitutes a breaking change
| Change type | Breaking? |
|---|---|
| Adding a new optional field to a response | No |
| Adding a new optional field to a request | No |
| Adding a new endpoint | No |
| Removing a field from a response | Yes |
| Changing a field type | Yes |
| Making an optional field required | Yes |
| Changing enum values | Yes |
| Changing HTTP status codes | Yes |
| Removing an endpoint | Yes |
| Changing authentication scheme | Yes |
Version lifecycle policy
Establish a written deprecation policy before publishing v1:
Version support commitment:
- A version remains supported for at minimum 12 months after the release of its successor
- Deprecation notices are provided in response headers (Deprecation, Sunset) at least 6 months before end-of-life
- Breaking changes are never made to a supported version
Deprecation headers (RFC 8594)
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: <https://api.example.com/v2/orders/123>; rel="successor-version"
Version routing pattern
// Route versioned requests to the appropriate handler
router.use('/v1', v1Router);
router.use('/v2', v2Router);
// Share business logic — only the request/response shape differs
// v1/orders.ts and v2/orders.ts call the same OrderService
Avoid duplicating business logic across versions. Version the transport contract (request/response shapes); reuse the domain/service layer.
Designing for additive evolution
Many breaking changes can be avoided with forward-compatible design:
- Use optional fields with sensible defaults rather than required fields where possible
- Accept and ignore unknown fields (
x-unknown-fields: ignorepolicy for consumers) - Use extensible enumerations (consumers should handle unknown enum values gracefully)
- Return structured objects rather than primitive values so you can add fields later