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
}
}