API Design
Spring Boot
OpenAPI
REST
Documentation

Modern API Design Patterns with Spring Boot and OpenAPI

Learn how to design clean, maintainable REST APIs using Spring Boot, covering documentation, versioning, error handling, and security best practices.

5 December 2024
10 min read

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[] 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.

Found this helpful?

I'd love to hear your thoughts or discuss similar topics. Feel free to reach out!