Versioning is a critical aspect of API design that allows you to evolve your API without breaking existing clients.
These are the following ways in which we can do the API versioning -
URI versioning
Header versioning
Content negotiation
Version migration strategies
1. URI Path Versioning
This is the most straightforward approach, where the URL path includes the version details.
Example
GET /v1/users
GET /v2/users
Pros
Clear and Explicit: The version is immediately visible to developers in the URI.
Cache-Friendly: Works well with caching systems since the version is part of the URL.
Backward Compatibility: Allows clients to continue using older versions while new versions are introduced.
Cons
Clutters URLs: Adds redundancy to the URI.
Limited Flexibility: Changing versions requires updating the client’s integration.
@RestController@RequestMapping("/api/v1")publicclassOrderControllerV1 { @GetMapping("/orders/{orderId}")publicResponseEntity<OrderResponseV1> getOrder(@PathVariableString orderId) {// V1 implementationreturnResponseEntity.ok(orderService.getOrderV1(orderId)); // v1 service }}@RestController@RequestMapping("/api/v2")publicclassOrderControllerV2 { @GetMapping("/orders/{orderId}")publicResponseEntity<OrderResponseV2> getOrder(@PathVariableString orderId) {// V2 implementation with enhanced responsereturnResponseEntity.ok(orderService.getOrderV2(orderId)); // v2 service }}
2. Header Versioning
This approach uses custom headers to specify the API version.
Pros
Clean URIs: Keeps the URL clean and focused on resource identification.
Flexible: Allows finer-grained control, such as specifying minor versions (1.1, 1.2).
Cons
Less Discoverable: The version information is not visible in the URL.
Cache Complexity: Requires more sophisticated caching mechanisms since caching is based on headers.
Best Practices
Use the Accept header with media type versioning.
Example:Accept: application/vnd.api+json; version=2.0
Alternatively, use a custom header.
Example:X-API-Version: 2
@RestController@RequestMapping("/api/orders")publicclassOrderController { @GetMapping("/{orderId}")publicResponseEntity<?> getOrder( @PathVariableString orderId, @RequestHeader(value ="X-API-Version", defaultValue ="1") int version) {switch (version) {case1:OrderResponseV1 responseV1 =orderService.getOrderV1(orderId);returnResponseEntity.ok(responseV1);case2:OrderResponseV2 responseV2 =orderService.getOrderV2(orderId);returnResponseEntity.ok(responseV2);default:thrownewUnsupportedVersionException("API version "+ version +" is not supported"); } }}// Version handler configuration@ConfigurationpublicclassVersioningConfig { @BeanpublicWebMvcConfigurerversioningConfigurer() {returnnewWebMvcConfigurer() { @OverridepublicvoidaddInterceptors(InterceptorRegistry registry) {registry.addInterceptor(newApiVersionInterceptor()); } }; }}
Api Version Interceptor
importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.ModelAndView;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.http.HttpStatus;publicclassApiVersionInterceptorimplementsHandlerInterceptor {privatestaticfinalLogger logger =LoggerFactory.getLogger(ApiVersionInterceptor.class);privatestaticfinalString VERSION_HEADER ="X-API-Version";privatestaticfinalint MIN_VERSION =1;privatestaticfinalint MAX_VERSION =2; @OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throwsException {String versionHeader =request.getHeader(VERSION_HEADER);// If no version header is provided, default to version 1if (versionHeader ==null||versionHeader.isEmpty()) {request.setAttribute("api-version",1);logger.debug("No API version specified. Defaulting to version 1");returntrue; }try {int version =Integer.parseInt(versionHeader);// Validate version rangeif (version < MIN_VERSION || version > MAX_VERSION) {logger.warn("Unsupported API version requested: {}", version);response.setStatus(HttpStatus.BAD_REQUEST.value());response.getWriter().write(String.format("API version %d is not supported. Supported versions are %d through %d", version, MIN_VERSION, MAX_VERSION) );returnfalse; }// Store the version in request attributes for later userequest.setAttribute("api-version", version);logger.debug("API Version {} validated successfully", version);returntrue; } catch (NumberFormatException e) {logger.error("Invalid API version format: {}", versionHeader);response.setStatus(HttpStatus.BAD_REQUEST.value());response.getWriter().write("Invalid API version format. Version must be a number");returnfalse; } } @OverridepublicvoidpostHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView) throwsException {// Add version header to response for clarityint version = (int) request.getAttribute("api-version");response.setHeader(VERSION_HEADER,String.valueOf(version)); } @OverridepublicvoidafterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) throwsException {// Cleanup if neededrequest.removeAttribute("api-version"); }}
3. Accept Header Versioning (Content Negotiation)
This strategy uses content negotiation through the Accept header. It determines the API version based on the Content-Type or Accept headers.
Example
GET /usersAccept:application/vnd.company.users+json; version=1
Pros
Advanced Flexibility: Supports versioning, content formats, and feature negotiation simultaneously.
Clean URIs: Does not add version information to the URL.
Cons
Complex Implementation: More challenging to implement and configure on the server side.
Discoverability: Version information is less obvious compared to URI versioning.
Best Practices
Use MIME types for content negotiation.
Example:application/vnd.company.resource-v1+json
Clearly document available content types and their associated versions.
While not as common, some APIs use query parameters for versioning.
@RestController@RequestMapping("/api/orders")publicclassOrderController { @GetMapping("/{orderId}")publicResponseEntity<?> getOrder( @PathVariableString orderId, @RequestParam(defaultValue ="1") int version) {returnswitch (version) {case1->ResponseEntity.ok(orderService.getOrderV1(orderId));case2->ResponseEntity.ok(orderService.getOrderV2(orderId));default->thrownewUnsupportedVersionException("Version not supported"); }; }}
Advanced Versioning Strategies
Feature Toggles with Versioning
@ServicepublicclassOrderService {privatefinalFeatureToggleService featureToggleService;publicOrderResponsegetOrder(String orderId,int version) {Order order =orderRepository.findById(orderId).orElseThrow(() ->newResourceNotFoundException("Order not found"));OrderResponse.OrderResponseBuilder builder =OrderResponse.builder().orderId(order.getId()).status(order.getStatus());// Add features based on version and togglesif (version >=2&&featureToggleService.isEnabled("ENHANCED_ORDER_DETAILS")) {builder.enhancedDetails(getEnhancedDetails(order)); }if (version >=2&&featureToggleService.isEnabled("ORDER_ANALYTICS")) {builder.analytics(getOrderAnalytics(order)); }returnbuilder.build(); }}