package com.example.demo.controller;
import com.example.demo.dto.UserDTO;
import com.example.demo.dto.v2.UserDTOV2;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
private final UserService userService;
public UserControllerV1(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserDTO> getUsers() {
return userService.getAllUsersV1();
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserV1(id);
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
private final UserService userService;
public UserControllerV2(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserDTOV2> getUsers() {
return userService.getAllUsersV2();
}
@GetMapping("/{id}")
public UserDTOV2 getUser(@PathVariable Long id) {
return userService.getUserV2(id);
}
// New endpoint in V2
@GetMapping("/{id}/profile")
public UserProfile getUserProfile(@PathVariable Long id) {
return userService.getUserProfile(id);
}
}
package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class VersionedUserController {
private final UserService userService;
public VersionedUserController(UserService userService) {
this.userService = userService;
}
@GetMapping(headers = "X-API-Version=1")
public List<UserDTO> getUsersV1() {
return userService.getAllUsersV1();
}
@GetMapping(headers = "X-API-Version=2")
public List<UserDTOV2> getUsersV2() {
return userService.getAllUsersV2();
}
}
package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class ContentNegotiationController {
private final UserService userService;
public ContentNegotiationController(UserService userService) {
this.userService = userService;
}
@GetMapping(produces = "application/vnd.myapi.v1+json")
public List<UserDTO> getUsersV1() {
return userService.getAllUsersV1();
}
@GetMapping(produces = "application/vnd.myapi.v2+json")
public List<UserDTOV2> getUsersV2() {
return userService.getAllUsersV2();
}
}
package com.example.demo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class ApiVersionInterceptor implements HandlerInterceptor {
private static final String VERSION_HEADER = "X-API-Version";
private static final String DEFAULT_VERSION = "1";
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
String version = request.getHeader(VERSION_HEADER);
if (version == null || version.isEmpty()) {
request.setAttribute("api.version", DEFAULT_VERSION);
} else {
request.setAttribute("api.version", version);
}
// Add deprecation warning for old versions
if ("1".equals(version)) {
response.addHeader("X-API-Warn",
"API version 1 is deprecated. Please migrate to version 2.");
}
return true;
}
}
API versioning manages evolution while supporting existing clients. URI versioning uses paths—/api/v1/users, /api/v2/users. Header versioning employs custom headers—X-API-Version: 2. Content negotiation uses Accept headers—application/vnd.myapi.v2+json. Query parameter versioning—/api/users?version=2. I prefer URI versioning for simplicity and visibility. Versioning enables breaking changes without disrupting clients. Deprecation warnings guide migrations. Version-specific controllers or routing handle differences. Shared logic avoids duplication. Documentation clearly indicates version differences. Proper versioning strategy balances backward compatibility with forward progress. It's essential for public APIs and long-lived applications.