API Versioning Strategies (URI vs Header vs Media Type)
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.