Hello! Welcome to the next lesson in our journey to becoming a backend developer.
In our last lesson, we focused on designing consistent and predictable JSON payloads. We learned how to use the envelope pattern for successful responses and a structured format like RFC 9457 for errors. This ensures our API communicates clearly with its clients. But what happens when the API itself needs to change? An application's requirements are never static; we'll always need to add features, modify data structures, or fix design flaws.
Today, we'll address this crucial challenge. This lesson is about how to version your REST APIs using URI-based and header-based strategies. By the end of this session, you'll understand why versioning is essential for managing an API's lifecycle and how to implement the most common versioning techniques in a Spring Boot application. This skill is vital for building robust, long-lived applications that can evolve without breaking the services and clients that depend on them.
1. Why Do We Need API Versioning?
Imagine you've released your API and it's being used by a mobile app. Now, your team decides to make a "breaking change." For example, you might want to split a single name field in a user profile into firstName and lastName. If you simply deploy this change, the existing mobile app, which expects a name field, will crash.
API versioning solves this by allowing you to expose multiple versions of your API simultaneously. Old clients can continue using the old version (v1), while new clients can take advantage of the new version (v2).
To understand the core concepts behind this, let's start with a short video that explains explicit versioning and introduces Semantic Versioning (SemVer), a standard for numbering your versions.
Web API - Explicit Versioning | URI vs Headers vs Request Params | SemVer | Sunset Headers
This video from the Ambient Coder channel provides a clear theoretical foundation for API versioning. It explains what explicit versioning is and how to label your versions meaningfully.
Please watch the following two segments: What is Explicit Versioning? (00:47 - 01:42): Focus on how this strategy allows you to introduce breaking changes safely. Semantic Versioning (04:49 - 06:16): Pay attention to the distinction between MAJOR, MINOR, and PATCH versions. This is the industry standard for communicating the nature of changes.
As you just saw, SemVer (MAJOR.MINOR.PATCH, e.g., 2.1.0) gives us a powerful language to communicate changes:
- MAJOR version for incompatible, breaking changes.
- MINOR version for adding new functionality in a backward-compatible way.
- PATCH version for backward-compatible bug fixes.
Most of the time in API versioning, you'll be dealing with major version changes (v1, v2, etc.) because these signify breaking changes that clients must adapt to.
2. Common Versioning Strategies
There are several ways a client can tell your server which version of the API it wants to use. We'll focus on the two most popular: URI versioning and header versioning. The image below provides a great at-a-glance summary of the common techniques.

Let's explore URI and Header versioning in more detail.
A. URI Versioning (or Path Versioning)
This is the most straightforward and common method. The version number is placed directly in the URI.
https://api.example.com/v1/usershttps://api.example.com/v2/users
Pros:
- Explicit and Clear: It's immediately obvious which version you are calling just by looking at the URL.
- Cache-friendly: Different versions have different URLs, which works well with standard HTTP caching mechanisms.
- Easy to use: You can simply paste the URL into a browser to test it.
Cons:
- "Clutters" the URI: Some argue that it violates the principle that a URI should represent a unique resource, not a version of it.
- Can lead to more code duplication: You might end up with separate controller classes or methods for each version.
B. Header Versioning
With this strategy, the URI stays "clean," and the client specifies the version in an HTTP request header. You can use a standard header like Accept or a custom header. A custom header is very common.
- Request to
https://api.example.com/userswith headerX-API-Version: 1 - Request to
https://api.example.com/userswith headerX-API-Version: 2
Pros:
- Clean URIs: The URI for a resource remains the same across all versions.
- Keeps versioning separate from resource location: This is considered a purer RESTful approach by some.
Cons:
- Less visible: You can't see the version by looking at the URL. It requires inspecting the request headers, making casual browsing or debugging slightly more difficult.
- Client complexity: The client developer must remember to add the header to every request.
3. Implementing Versioning in Spring Boot
Theory is great, but let's see how this works in code. Fortunately, modern versions of Spring Boot (3.2+) and Spring Framework (6.1+) have introduced native support for API versioning, making it much simpler than it used to be.
The following video by Dan Vega is an excellent, practical walkthrough of how to implement both URI and header versioning using this new native support.
API Versioning Made Simple with Spring Boot 4.0
In this tutorial, Dan Vega demonstrates how to use Spring Boot's new built-in API versioning capabilities. This is a perfect hands-on guide for our lesson.
Please watch these segments carefully. The code is the most important part. Configuration (09:57 - 13:53): Watch how to set up the configuration in a WebConfig class. Pay close attention to addSupportedVersions, setDefaultVersion, usePathSegment(), and useRequestHeader(). URI (Path Segment) Versioning (13:53 - 17:42): This is the core of the URI versioning implementation. Notice how a version placeholder is used in @RequestMapping and how different methods handle v1 and v2. The example of splitting the name field is a perfect illustration of a breaking change. Header Versioning (20:45 - 24:51): See how the configuration is switched to useRequestHeader() and how the client sends a custom X-API-Version header to select the desired API version.
Let's recap the key implementation steps from the video.
To Implement URI Versioning:
-
Create a Configuration Class:
Create a class that implementsWebMvcConfigurer.@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersioningConfigurer configurer) { configurer.usePathSegment(1) // Use the path segment at index 1 .addSupportedVersions(1, 2) .setDefaultVersion(1); } }Note: The video uses version strings like
"1.0.0", but simple integers also work. -
Update Your Controller:
Use a version placeholder in the class-level@RequestMappingand specify the version on each method.@RestController @RequestMapping("/api/{version}/users") // version placeholder public class UserController { @GetMapping(version = "1") // This handles /api/1/users public List<UserDtoV1> getAllUsersV1() { // ... return DTO with single 'name' field } @GetMapping(version = "2") // This handles /api/2/users public List<UserDtoV2> getAllUsersV2() { // ... return DTO with 'firstName' and 'lastName' fields } }
To Implement Header Versioning:
-
Update the Configuration:
ChangeusePathSegmenttouseRequestHeaderand specify the header name.@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersioningConfigurer configurer) { configurer.useRequestHeader("X-API-Version") // Specify header name .addSupportedVersions(1, 2) .setDefaultVersion(1); } } -
Update Your Controller:
The class-level mapping becomes "clean" again. Spring will route requests based on the header value.@RestController @RequestMapping("/api/users") // No version placeholder public class UserController { @GetMapping(version = "1") public List<UserDtoV1> getAllUsersV1() { // ... logic for v1 } @GetMapping(version = "2") public List<UserDtoV2> getAllUsersV2() { // ... logic for v2 } }
For a quick textual reference and more code examples of these strategies, the following article from Baeldung is a great resource to bookmark.
This article from Baeldung, a trusted source for Java developers, provides a solid textual overview of API versioning in Spring.
Quickly scan through these sections: Section 3.1 'URI Versioning (Path-Based)': Note the simple example using two separate controller classes. Section 3.3 'Header Versioning': See how the headers attribute of @GetMapping can be used for versioning (this is an alternative to the newer native support we just saw). Section 5 'Common API Versioning Strategies in Spring': The summary table here is an excellent recap of the pros and cons.
Test your understanding!
You are tasked with evolving an API for a blog platform. The current endpoint GET /posts/{id} returns a Post object with a content field containing the full blog post as a single string.
The new requirement is to change content from a single string to a structured object: { "summary": "...", "body": "..." }. This is a breaking change.
- Which versioning strategy (URI or Header) would you choose to make this change as explicit and clear as possible for the API consumers?
- Write the new URI for accessing version 2 of a post with
id=123. - In your Spring Boot controller, what would the
@GetMappingannotation look like for the new version 2 method, assuming you use URI versioning?
Show answer
-
URI versioning is the best choice here. Because this is a significant breaking change, making it explicit in the URL (
/v2/) is the clearest way to signal to developers that the response structure has fundamentally changed. -
The new URI would be:
/api/v2/posts/123 -
Assuming the class is mapped to
/api/{version}/posts, the method annotation would be:@GetMapping(path = "/{id}", version = "2") public PostDtoV2 getPostByIdV2(@PathVariable Long id) { // ... logic to return the new DTO with summary and body fields }
Conclusion
You've now learned how to manage one of the most inevitable aspects of software development: change. By versioning your APIs, you can innovate and improve your backend services without disrupting the clients that rely on them.
Key Takeaways:
- Versioning is essential for API evolution. It allows you to introduce breaking changes without affecting existing consumers.
- Semantic Versioning (
MAJOR.MINOR.PATCH) is the standard for communicating the impact of your changes. - URI Versioning (
/v1/resource) is explicit and widely used, especially for major, breaking changes. - Header Versioning (
X-API-Version: 1) keeps URIs clean and is another popular and valid strategy. - Spring Boot provides excellent native support to implement these strategies in a clean and declarative way.
What's Next?
In the examples today, you saw that a key part of versioning was creating different Java objects for each version (UserDtoV1, UserDtoV2). These objects are called Data Transfer Objects (DTOs). They act as a contract for your API and are crucial for decoupling what you expose to the world from your internal database models.
In our next lesson, we will dive deep into DTOs. You will learn how to manually design and implement Data Transfer Objects to decouple your API from your domain models, a practice that is fundamental to building clean, maintainable, and scalable backend applications.