PushBackLog

API Versioning

Soft enforcement Complete by PushBackLog team
Topic: architecture Topic: api-design Skillset: backend Technology: generic Stage: planning Stage: execution Stage: review

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 typeBreaking?
Adding a new optional field to a responseNo
Adding a new optional field to a requestNo
Adding a new endpointNo
Removing a field from a responseYes
Changing a field typeYes
Making an optional field requiredYes
Changing enum valuesYes
Changing HTTP status codesYes
Removing an endpointYes
Changing authentication schemeYes

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: ignore policy 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