Modern API Design Patterns with Spring Boot and OpenAPI
Introduction to Modern API Design
Building well-designed APIs is crucial for creating maintainable, scalable applications. This guide explores modern API design patterns using Spring Boot and OpenAPI, covering documentation, versioning, error handling, and security best practices.
Core Design Principles
RESTful Architecture
Follow REST principles for predictable and intuitive APIs:
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
@GetMapping
public ResponseEntity> getUsers(
@Valid @ModelAttribute UserSearchCriteria criteria,
@Valid @ModelAttribute PageRequest pageRequest) {
Page users = userService.findUsers(criteria, pageRequest);
PagedResponse response = PagedResponse.of(
users.map(userMapper::toDto)
);
return ResponseEntity.ok(response);
}
@PostMapping
public ResponseEntity createUser(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
URI location = linkTo(methodOn(UserController.class)
.getUser(user.getId())).toUri();
return ResponseEntity.created(location)
.body(userMapper.toDto(user));
}
}
OpenAPI Documentation
Use SpringDoc OpenAPI for comprehensive API documentation:
@Operation(
summary = "Create a new user",
description = "Creates a new user account with the provided information"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "User created successfully",
content = @Content(schema = @Schema(implementation = UserDto.class))
),
@ApiResponse(
responseCode = "400",
description = "Invalid input data",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "409",
description = "User already exists",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
})
@PostMapping
public ResponseEntity createUser(
@Parameter(description = "User creation request")
@Valid @RequestBody CreateUserRequest request) {
// Implementation
}
Configuration
@Configuration
@EnableWebSecurity
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("User Management API")
.version("1.0.0")
.description("A comprehensive API for user management operations")
.contact(new Contact()
.name("API Support")
.email("api-support@example.com"))
.license(new License()
.name("MIT")
.url("https://opensource.org/licenses/MIT")))
.addSecurityItem(new SecurityRequirement()
.addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.name("bearerAuth")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
Error Handling and Validation
Global Exception Handler
Implement consistent error responses across your API:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity handleValidationErrors(
MethodArgumentNotValidException ex) {
List fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> FieldError.builder()
.field(error.getField())
.message(error.getDefaultMessage())
.rejectedValue(error.getRejectedValue())
.build())
.collect(toList());
ErrorResponse response = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Failed")
.message("Input validation failed")
.fieldErrors(fieldErrors)
.path(getCurrentPath())
.build();
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity handleEntityNotFound(
EntityNotFoundException ex) {
ErrorResponse response = ErrorResponse.builder()
.timestamp(Instant.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Resource Not Found")
.message(ex.getMessage())
.path(getCurrentPath())
.build();
return ResponseEntity.notFound().build();
}
}
Custom Validation
Create custom validation annotations for business rules:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email address already exists";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
@Component
public class UniqueEmailValidator implements ConstraintValidator {
private final UserRepository userRepository;
@Override
public boolean isValid(CreateUserRequest request, ConstraintValidatorContext context) {
return request.getEmail() == null ||
!userRepository.existsByEmail(request.getEmail());
}
}
API Versioning
Implement version management for backward compatibility:
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
@GetMapping("/{id}")
public ResponseEntity getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(userV1Mapper.toDto(user));
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
@GetMapping("/{id}")
public ResponseEntity getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(userV2Mapper.toDto(user));
}
}
Security Best Practices
JWT Authentication
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v/auth/").permitAll()
.requestMatchers("/swagger-ui/
", "/v3/api-docs/").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v
/users/*").hasRole("USER")
.requestMatchers(HttpMethod.POST, "/api/v
/users/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtDecoder(jwtDecoder())))
.build();
}
}
Rate Limiting
Implement rate limiting to prevent abuse:
@Component
public class RateLimitingFilter implements Filter {
private final RedisTemplate redisTemplate;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String clientId = getClientIdentifier(httpRequest);
String key = "rate_limit:" + clientId;
String currentCount = redisTemplate.opsForValue().get(key);
int requests = currentCount != null ? Integer.parseInt(currentCount) : 0;
if (requests >= 100) { // 100 requests per hour
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.getWriter().write("Rate limit exceeded");
return;
}
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, Duration.ofHours(1));
chain.doFilter(request, response);
}
}
Testing Your API
Integration Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {
@Container
static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:14");
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateUserSuccessfully() {
CreateUserRequest request = CreateUserRequest.builder()
.email("test@example.com")
.firstName("John")
.lastName("Doe")
.build();
ResponseEntity response = restTemplate.postForEntity(
"/api/v1/users", request, UserDto.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getEmail()).isEqualTo("test@example.com");
}
}
Conclusion
Modern API design with Spring Boot and OpenAPI enables you to build robust, well-documented, and maintainable APIs. Key practices include:
- Follow RESTful principles
- Comprehensive OpenAPI documentation
- Consistent error handling and validation
- Proper versioning strategy
- Security best practices
- Thorough testing
By implementing these patterns, you'll create APIs that are easy to use, maintain, and scale as your application grows.