How to manage REST API versioning with spring?
Regardless whether versioning can be avoided by doing backwards compatible changes (which might not always possible when you are bound by some corporate guidelines or your API clients are implemented in a buggy way and would break even if they should not) the abstracted requirement is an interesting one:
How can I do a custom request mapping that does arbitrary evaluations of header values from the request without doing the evaluation in the method body?
As described in this SO answer you actually can have the same @RequestMapping
and use a different annotation to differentiate during the actual routing that happens during runtime. To do so, you will have to:
- Create a new annotation
VersionRange
. - Implement a
RequestCondition<VersionRange>
. Since you will have something like a best-match algorithm you will have to check whether methods annotated with otherVersionRange
values provide a better match for the current request. - Implement a
VersionRangeRequestMappingHandlerMapping
based on the annotation and request condition (as described in the post How to implement @RequestMapping custom properties
). - Configure spring to evaluate your
VersionRangeRequestMappingHandlerMapping
before using the defaultRequestMappingHandlerMapping
(e.g. by setting its order to 0).
This wouldn't require any hacky replacements of Spring components but uses the Spring configuration and extension mechanisms so it should work even if you update your Spring version (as long as the new version supports these mechanisms).
spring rest api versioning
If you want to do use separate method for both version you can by defining different value in @RequestMapping
method 1
@RequestMapping(
value = "baseurl/v1/role",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
and
@RequestMapping(
value = "baseurl/v2/role",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
but if you want to handle it in same method you can using
method 2: use @PathVariable
@RequestMapping(
value = "baseurl/{version}/role",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public returnType methodName(@PathVariable("version") String version){
// code to check version
}
method 3: use @RequestHeader
@RequestMapping(
value = "baseurl/role",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public returnType methodName(@RequestHeader("version") String version){
// code to check version
}
But as you said you have different version of api's prefer to manage them in separate controller using @RequestMapping
at class level
method 4:
@RestController
@RequestMapping("/v1")
public class anyController {
}
and
@RestController
@RequestMapping("/v2")
public class anyController {
}
How to manage REST API versioning with spring data rest?
This is an interesting topic. The documentation of spring-data-rest does not mention best-practices for versioning.
From what I know about spring-data-rest it is not possible to implement media type versioning without implementing custom controllers.
But of course the current functionality allows for URI based versioning:
- always include a version in your entity - either in the package or class name - e.g.
com.company.v1.MyEntity
orcom.company.MyEntityV1
in the repository use
@RepositoryRestResource
to contain the version in the URI@RepositoryRestResource(path = "/v1/shops")
So you could always create a new entity version when you have breaking changes on the API and use a new repository to expose the new version on the API.
EDIT:
We were discussing this topic in the team and a few new interesting thoughts on REST API versioning came up that I would like to share.
I think we all agree that the idea outlined above is not an elegant one. It introduces a lot of code duplication and when you switch off the old version you have to remove it from the code base - which can be harder than you think it is. In the meantime you have to maintain two versions in the code base. You would always want to avoid that.
So what are the alternatives. You could do what we often do in a microservices world - push complexity to the infrastructure. Creating and running new runtimes is fairly easy in a microservices infrastructure so we could choose to run two different versions of the same service. The old version providing the old API version and the new one supporting our new version. An API Gateway could take over the routing e.g. based on HTTP headers (Accept or Content-Type) or version information contained in the URI. And if you want to get rid of the old version you just have to switch off the runtimes that run the old version and you are done. I think this can be an elegant solution that keeps your code base clean.
There are chances that your infrastructure already supports running different versions of the same service to support blue-green deployment scenarios. Which means you already have some routing capabilities that you could build up on.
Also I think if you support blue-green deployment scenarios (which you should) you have to keep database schema versions compatible between releases (e.g. your old version of your service needs to be able to run on the new schema version). If you have to keep this level of compatibility it might not be too hard to avoid REST API versioning most of the times. So you are not forced to do it often - maybe only if you make fundamental changes (that you would not do in the same codebase anyway).
Custom Header Approach for Spring Boot REST API versioning
this is how this approach works.
@RestController
public class TestController {
@RequestMapping(value = "/user")
public String getUserDefault() {
return "getUserDefault";
}
@RequestMapping(value = "/user", headers = {"X-API-VERSION=v1"})
public String getUserV1() {
return "getUserV1";
}
@RequestMapping(value = "/user", headers = {"X-API-VERSION=v2"})
public String getUserV2() {
return "getUserV2";
}
}
also, you can use GetMapping
instead of RequestMapping
, let's test it using curl
:
curl --location --request GET 'http://localhost:8080/user' --header 'X-API-VERSION: v2'
return 200 getUserV2
curl --location --request GET 'http://localhost:8080/user' --header 'X-API-VERSION: v1'
return 200 getUserV1
curl --location --request GET 'http://localhost:8080/user'
return 200 getUserDefault
Related Topics
Integer.Valueof() VS. Integer.Parseint()
How to Manage Exceptions Thrown in Filters in Spring
How to Build Sources Jar with Gradle
What Does the "Assert" Keyword Do
How to Create a Sub Array from Another Array in Java
Spring Cache @Cacheable - Not Working While Calling from Another Method of the Same Bean
Generic Type Parameter Naming Convention for Java (With Multiple Chars)
Timer & Timertask Versus Thread + Sleep in Java
Why Can't I Declare Static Methods in an Interface
Synchronization of Non-Final Field
Immutable VS Unmodifiable Collection
Should Private Helper Methods Be Static If They Can Be Static