API Versioning Strategies (URI vs Header vs Media Type)#
Versioning is unavoidable once multiple clients depend on your API. A durable strategy must preserve backward compatibility, enable safe rollouts, and support client-specific migrations without breaking caches or observability.
Goals of a Versioning Strategy#
A strong versioning strategy should:
- Make breaking changes explicit and discoverable.
- Allow old and new clients to coexist.
- Keep documentation and SDKs in sync.
- Avoid routing ambiguity and cache pollution.
URI Versioning#
Example: /v1/orders and /v2/orders.
Pros:
- Simple to route and cache.
- Easy to observe in logs and metrics.
- Works well with API gateways and CDN caching.
Cons:
- URL changes propagate everywhere.
- Harder to do per-field deprecation within the same version.
- May encourage coarse-grained version bumps.
Header Versioning#
Example: X-Api-Version: 2.
Pros:
- URLs stay stable.
- Supports granular or per-client versioning.
- Easier to keep links stable in documentation.
Cons:
- Harder to cache and debug.
- Non-standard headers can be blocked by proxies.
- Requires more attention in client SDKs.
Media Type (Content Negotiation) Versioning#
Example: Accept: application/vnd.example.orders+json;version=2.
Pros:
- Aligns with HTTP content negotiation semantics.
- Allows multiple representations of the same resource.
- Works well for evolutionary schema changes.
Cons:
- Complex to implement correctly.
- Harder for humans to inspect and debug.
- Requires strict documentation discipline.
Recommendation for Advanced Systems#
Use URI versioning for major breaking changes and media type versioning for gradual, representation-level changes. This gives a clear upgrade path while enabling more granular control inside a version.
Spring Boot Routing Examples#
1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/v1/orders")
class OrderV1Controller {
@GetMapping
public List<OrderV1> listOrders() {
return List.of();
}
}
1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/orders")
class OrderHeaderVersionController {
@GetMapping(headers = "X-Api-Version=2")
public List<OrderV2> listOrdersV2() {
return List.of();
}
}
1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/orders")
class OrderMediaTypeVersionController {
@GetMapping(produces = "application/vnd.example.orders+json;version=2")
public List<OrderV2> listOrdersV2() {
return List.of();
}
}
Version Lifecycle Management#
A predictable lifecycle reduces operational risk:
- Deprecation headers: add
DeprecationandSunsetheaders. - Dual-write or transform: write data in a backward-compatible shape during migration.
- Traffic shaping: use the API gateway to gradually migrate clients.
Observability and Compatibility Testing#
Implement contract tests that run against both versions. Track version usage in metrics to know when it is safe to retire older versions.
Summary#
URI versioning is operationally simplest, header versioning is client-friendly, and media type versioning is semantically correct but more complex. Mature APIs often mix these strategies to balance compatibility with operational clarity.