Mastering API Versioning in Spring Boot: URI vs Header vs Media-Type

"If your API doesn’t evolve, your product probably isn’t either."
In the real world, breaking changes to your APIs are often unavoidable. The key is how you handle them. That’s where API versioning comes in — ensuring that older clients still work, even as your backend evolves.
This comprehensive guide dives deep into three robust ways to implement API versioning in Spring Boot:
URI Path Versioning
Header Versioning
Media-Type Versioning
? Why API Versioning is Essential
APIs are contracts. Changing them without notice breaks clients, results in poor user experience, and often leads to production issues. Versioning is your insurance policy against this chaos.
👍 Versioning helps:
Prevent breaking existing clients when APIs change
Introduce new features safely
Support legacy systems during migration
Maintain multiple API versions simultaneously
Let’s take a real-world example:
You initially exposed a
GET /users
API returning the user's name and country. Now you want to include their job role as well.
You now have two choices:
Break the response format (bad)
Version the API (best practice)
Let's see how you'd handle this change across the different strategies.
The Original Version - V1
UserV1
public class UserV1 {
private String name;
private String country;
public UserV1(String name, String country) {
this.name = name;
this.country = country;
}
// Getters and Setters
}
Controller for V1 (URI versioning shown here)
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
@GetMapping
public UserV1 getUser() {
return new UserV1("XYZ", "India");
}
}
Adding a New Field: role (V2)
UserV2
public class UserV2 {
private String name;
private String country;
private String role;
public UserV2(String name, String country, String role) {
this.name = name;
this.country = country;
this.role = role;
}
// Getters and Setters
}
URI Path Versioning
🔧 Sample Endpoint:
GET /api/v2/users
Code Example:
@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
@GetMapping
public UserV2 getUser() {
return new UserV2("XYZ", "India", "Java Developer");
}
}
👍Pros:
Simple to implement
Version is visible and intuitive in the URL
Easy to test via browser or Postman
👎Cons:
Violates REST principles (version is not part of the resource)
Bloats endpoint URLs over time
Requires new controller classes per version
Header Versioning
🔧 Sample Request:
GET /api/users
Header: X-API-VERSION: 2
Code Example:
@RestController
@RequestMapping("/api/users")
public class UserHeaderVersioningController {
@GetMapping(headers = "X-API-VERSION=1")
public UserV1 getUserV1() {
return new UserV1("XYZ", "India");
}
@GetMapping(headers = "X-API-VERSION=2")
public UserV2 getUserV2() {
return new UserV2("XYZ", "India", "Java Developer");
}
}
👍 Pros:
Keeps URLs clean
More REST-aligned than URI versioning
Easy to add new versions
👎 Cons:
Not directly accessible via browser
Requires custom headers
Not as user-friendly for external APIs
Media-Type Versioning
🔧 Sample Request:
GET /api/users
Header: Accept: application/xyz.myapp.v1+json
Code Example:
@RestController
@RequestMapping("/api/users")
public class UserMediaTypeVersioningController {
@GetMapping(produces = "application/xyz.app.v1+json")
public UserV1 getUserV1() {
return new UserV1("XYZ", "India");
}
@GetMapping(produces = "application/xyz.app.v2+json")
public UserV2 getUserV2() {
return new UserV2("XYZ", "India", "Java Developer");
}
}
👍 Pros:
Fully REST-compliant
Allows precise content negotiation
Clean separation of versions
👎 Cons:
Complex for beginners
Harder to test manually
Can confuse third-party API consumers
When to use, what ?
Scenario | Best Versioning Strategy |
External/Public API | URI versioning |
Internal Controlled clients | Header versioning |
Strict REST Adherence Needed | Media-type versioning |
🤝Let’s Connect!
Thanks for reading!
Subscribe to my newsletter
Read articles from Devanshu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
