What are common approaches to API versioning in Spring REST APIs?

7 minintermediateapi-versioningrestspring-mvc

Quick Answer

Common strategies include URI versioning (embedding the version in the path, e.g., /api/v1/orders — the simplest and most visible, but clutters the URL and complicates routing for shared resources), request header versioning (a custom header like Api-Version, keeping URLs stable), media type/content negotiation versioning (a custom Accept header value like application/vnd.company.v2+json), and query parameter versioning (?version=2). URI versioning is the most common in practice for its simplicity and discoverability, despite being the least 'RESTful' in a strict sense.

Detailed Answer

As a REST API evolves, breaking changes (renaming/removing a field, changing behavior) need a way to coexist with clients still using an older contract. Several strategies are common in Spring-based APIs:

1. URI versioning — embed the version directly in the path:

@RestController
@RequestMapping("/api/v1/orders")
class OrderControllerV1 { ... }

@RestController
@RequestMapping("/api/v2/orders")
class OrderControllerV2 { ... }

Simplest to implement and most discoverable/visible to API consumers (and easy to route, cache, and document per version), but clutters the URL, and strict REST purists argue a resource's URI shouldn't encode something as orthogonal as its representation's version.

2. Header versioning — a custom request header carries the version, URL stays stable:

@GetMapping(value = "/orders", headers = "Api-Version=2")
List<OrderV2> listOrdersV2() { ... }

Keeps URLs clean, but is less visible/discoverable (you can't just paste a URL into a browser to see a specific version) and slightly more effort to test/document.

3. Media type (content negotiation) versioning — the version lives inside a custom Accept header value:

@GetMapping(value = "/orders", produces = "application/vnd.company.app-v2+json")
List<OrderV2> listOrdersV2() { ... }

Considered the most "RESTful" by strict HATEOAS/content-negotiation advocates, but the least ergonomic for typical API consumers and tooling.

4. Query parameter versioning:

@GetMapping(value = "/orders", params = "version=2")
List<OrderV2> listOrdersV2() { ... }

Simple, but easy to omit accidentally (defaulting to some implicit version) and considered less clean than the path-based approach by many API design guides.

In practice: URI versioning is by far the most common choice for public and internal APIs alike, largely for its simplicity, visibility in logs/monitoring, and ease of routing at the infrastructure level (a load balancer or API gateway can trivially route /api/v1/* vs /api/v2/* to different backend deployments) — despite being the "least RESTful" option in a strict theoretical sense.