Versioning Strategies
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")
public class OrderControllerV1 {
@GetMapping("/orders/{orderId}")
public ResponseEntity<OrderResponseV1> getOrder(@PathVariable String orderId) {
// V1 implementation
return ResponseEntity.ok(orderService.getOrderV1(orderId)); // v1 service
}
}
@RestController
@RequestMapping("/api/v2")
public class OrderControllerV2 {
@GetMapping("/orders/{orderId}")
public ResponseEntity<OrderResponseV2> getOrder(@PathVariable String orderId) {
// V2 implementation with enhanced response
return ResponseEntity.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")
public class OrderController {
@GetMapping("/{orderId}")
public ResponseEntity<?> getOrder(
@PathVariable String orderId,
@RequestHeader(value = "X-API-Version", defaultValue = "1") int version) {
switch (version) {
case 1:
OrderResponseV1 responseV1 = orderService.getOrderV1(orderId);
return ResponseEntity.ok(responseV1);
case 2:
OrderResponseV2 responseV2 = orderService.getOrderV2(orderId);
return ResponseEntity.ok(responseV2);
default:
throw new UnsupportedVersionException("API version " + version + " is not supported");
}
}
}
// Version handler configuration
@Configuration
public class VersioningConfig {
@Bean
public WebMvcConfigurer versioningConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ApiVersionInterceptor());
}
};
}
}
Api Version Interceptor
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
public class ApiVersionInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ApiVersionInterceptor.class);
private static final String VERSION_HEADER = "X-API-Version";
private static final int MIN_VERSION = 1;
private static final int MAX_VERSION = 2;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String versionHeader = request.getHeader(VERSION_HEADER);
// If no version header is provided, default to version 1
if (versionHeader == null || versionHeader.isEmpty()) {
request.setAttribute("api-version", 1);
logger.debug("No API version specified. Defaulting to version 1");
return true;
}
try {
int version = Integer.parseInt(versionHeader);
// Validate version range
if (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)
);
return false;
}
// Store the version in request attributes for later use
request.setAttribute("api-version", version);
logger.debug("API Version {} validated successfully", version);
return true;
} 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");
return false;
}
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// Add version header to response for clarity
int version = (int) request.getAttribute("api-version");
response.setHeader(VERSION_HEADER, String.valueOf(version));
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// Cleanup if needed
request.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 /users
Accept: 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.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping(
value = "/{orderId}",
produces = "application/vnd.company.app-v1+json"
)
public ResponseEntity<OrderResponseV1> getOrderV1(@PathVariable String orderId) {
return ResponseEntity.ok(orderService.getOrderV1(orderId));
}
@GetMapping(
value = "/{orderId}",
produces = "application/vnd.company.app-v2+json"
)
public ResponseEntity<OrderResponseV2> getOrderV2(@PathVariable String orderId) {
return ResponseEntity.ok(orderService.getOrderV2(orderId));
}
}
4. Query Parameter Versioning
While not as common, some APIs use query parameters for versioning.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{orderId}")
public ResponseEntity<?> getOrder(
@PathVariable String orderId,
@RequestParam(defaultValue = "1") int version) {
return switch (version) {
case 1 -> ResponseEntity.ok(orderService.getOrderV1(orderId));
case 2 -> ResponseEntity.ok(orderService.getOrderV2(orderId));
default -> throw new UnsupportedVersionException("Version not supported");
};
}
}
Advanced Versioning Strategies
Feature Toggles with Versioning
@Service
public class OrderService {
private final FeatureToggleService featureToggleService;
public OrderResponse getOrder(String orderId, int version) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
OrderResponse.OrderResponseBuilder builder = OrderResponse.builder()
.orderId(order.getId())
.status(order.getStatus());
// Add features based on version and toggles
if (version >= 2 && featureToggleService.isEnabled("ENHANCED_ORDER_DETAILS")) {
builder.enhancedDetails(getEnhancedDetails(order));
}
if (version >= 2 && featureToggleService.isEnabled("ORDER_ANALYTICS")) {
builder.analytics(getOrderAnalytics(order));
}
return builder.build();
}
}
Graceful Deprecation
@RestController
@RequestMapping("/api/v1")
public class OrderControllerV1 {
@GetMapping("/orders/{orderId}")
@Deprecated
@Header("Deprecation: date=2024-12-31")
@Header("Link: </api/v2/orders/{orderId}>; rel=\"successor-version\"")
public ResponseEntity<OrderResponseV1> getOrder(@PathVariable String orderId) {
// Log deprecation metrics
deprecationMetricsService.recordDeprecatedEndpointUsage(
"GET /api/v1/orders/{orderId}");
return ResponseEntity.ok(orderService.getOrderV1(orderId));
}
}
Best Practices for API Versioning
Version Strategy Selection Consider these factors when choosing a strategy:
Client requirements
Infrastructure constraints
Developer experience
Maintenance overhead
Backwards Compatibility
public class OrderResponseV2 implements OrderResponse { // New fields private List<OrderItemV2> items; private EnhancedShippingDetails shipping; // Backward compatibility method public OrderResponseV1 toV1Format() { return OrderResponseV1.builder() .orderId(this.orderId) .status(this.status) .items(this.items.stream() .map(OrderItemV2::toV1Format) .collect(Collectors.toList())) .build(); } }
Documentation
@OpenAPIDefinition( info = @Info( title = "E-commerce API", version = "2.0", description = "API documentation with version support" ) ) @RestController public class OrderController { @Operation(summary = "Get order details", description = "Version 2 includes enhanced order details") @ApiResponse( responseCode = "200", description = "Order retrieved successfully", content = @Content(schema = @Schema(implementation = OrderResponseV2.class)) ) @GetMapping( value = "/api/v2/orders/{orderId}", produces = "application/json" ) public ResponseEntity<OrderResponseV2> getOrderV2(...) { // Implementation } }
Comparison of Strategies
Strategy
Visibility
Ease of Use
Flexibility
Cache Support
Common Use Case
URI Versioning
High (in URL)
Easy
Low
Excellent
Public APIs with simple versioning needs.
Header Versioning
Low (in headers)
Moderate
High
Complex
APIs requiring clean URLs and flexibility.
Content Negotiation
Low (in headers)
Complex
Very High
Moderate
Advanced APIs with multiple content types.
Last updated