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

  1. Version Strategy Selection Consider these factors when choosing a strategy:

    • Client requirements

    • Infrastructure constraints

    • Developer experience

    • Maintenance overhead

  2. 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();
        }
    }
  3. 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