Spring Boot REST APIs: Advanced Patterns

Introduction
In our previous post on Getting Started with Spring Boot, we built a simple REST API. Now, let's take it to the next level by exploring advanced patterns and best practices used in production environments.
What You'll Learn
✅ Implement robust exception handling and error responses
✅ Add request validation with Bean Validation
✅ Build pagination and filtering for large datasets
✅ Implement API versioning strategies
✅ Secure your API with Spring Security and JWT
✅ Generate OpenAPI/Swagger documentation
✅ Write comprehensive tests (Unit, Integration, E2E)
✅ Optimize API performance
Prerequisites
- Java 17 or higher installed
- Understanding of Spring Boot basics (see our getting started guide)
- Familiarity with REST API principles
- Maven or Gradle experience
1. Advanced Exception Handling
Global Exception Handler
Spring Boot's @ControllerAdvice allows centralized exception handling across all controllers.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now(),
request.getDescription(false)
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
errors,
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred",
LocalDateTime.now(),
request.getDescription(false)
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}Custom Error Response
@Data
@AllArgsConstructor
public class ErrorResponse {
private int status;
private String message;
private LocalDateTime timestamp;
private String path;
private Map<String, String> errors; // For validation errors
}Custom Exceptions
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, String field, Object value) {
super(String.format("%s not found with %s: '%s'", resource, field, value));
}
}
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}Usage in Controller:
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
return ResponseEntity.ok(user);
}2. Request Validation with Bean Validation
Adding Dependencies
<!-- Already included in spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>Entity Validation
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(
regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$",
message = "Password must contain uppercase, lowercase, digit, and special character"
)
private String password;
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 120, message = "Age must be less than 120")
private Integer age;
@Past(message = "Birth date must be in the past")
private LocalDate birthDate;
}DTO Validation
@Data
public class CreateUserRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, max = 100)
private String password;
@NotNull(message = "Age is required")
@Min(18)
@Max(120)
private Integer age;
}Controller Validation
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(
@PathVariable @Min(1) Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}Custom Validators
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired
private UserRepository userRepository;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
return email != null && !userRepository.existsByEmail(email);
}
}3. Pagination and Filtering
Pagination with Spring Data
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
Sort.Direction direction = sortDir.equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
Page<UserDTO> users = userService.getUsers(pageable);
return ResponseEntity.ok(users);
}
}Custom Pagination Response
@Data
@AllArgsConstructor
public class PageResponse<T> {
private List<T> content;
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
private boolean first;
}
// In service layer
public PageResponse<UserDTO> getUsers(Pageable pageable) {
Page<User> page = userRepository.findAll(pageable);
List<UserDTO> content = page.getContent().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return new PageResponse<>(
content,
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isLast(),
page.isFirst()
);
}Advanced Filtering with Specifications
public class UserSpecification {
public static Specification<User> hasUsername(String username) {
return (root, query, cb) ->
username == null ? null : cb.like(root.get("username"), "%" + username + "%");
}
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.like(root.get("email"), "%" + email + "%");
}
public static Specification<User> ageGreaterThan(Integer age) {
return (root, query, cb) ->
age == null ? null : cb.greaterThanOrEqualTo(root.get("age"), age);
}
public static Specification<User> ageLessThan(Integer age) {
return (root, query, cb) ->
age == null ? null : cb.lessThanOrEqualTo(root.get("age"), age);
}
}
// Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
// Controller
@GetMapping("/search")
public ResponseEntity<Page<UserDTO>> searchUsers(
@RequestParam(required = false) String username,
@RequestParam(required = false) String email,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
Pageable pageable) {
Specification<User> spec = Specification
.where(UserSpecification.hasUsername(username))
.and(UserSpecification.hasEmail(email))
.and(UserSpecification.ageGreaterThan(minAge))
.and(UserSpecification.ageLessThan(maxAge));
Page<User> users = userRepository.findAll(spec, pageable);
// Convert to DTO and return
}4. API Versioning Strategies
Strategy 1: URI Versioning (Recommended)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
// Version 1 implementation
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// Version 2 implementation with breaking changes
}Pros: Clear, easy to understand, widely used Cons: URL pollution, duplicate code
Strategy 2: Header Versioning
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(headers = "API-Version=1")
public ResponseEntity<UserDTOV1> getUserV1(@PathVariable Long id) {
// Version 1 logic
}
@GetMapping(headers = "API-Version=2")
public ResponseEntity<UserDTOV2> getUserV2(@PathVariable Long id) {
// Version 2 logic
}
}Request:
curl -H "API-Version: 2" http://localhost:8080/api/users/1Strategy 3: Content Negotiation
@GetMapping(produces = "application/vnd.company.v1+json")
public ResponseEntity<UserDTOV1> getUserV1(@PathVariable Long id) {
// Version 1 logic
}
@GetMapping(produces = "application/vnd.company.v2+json")
public ResponseEntity<UserDTOV2> getUserV2(@PathVariable Long id) {
// Version 2 logic
}Request:
curl -H "Accept: application/vnd.company.v2+json" http://localhost:8080/api/users/15. Security with Spring Security and JWT
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>JWT Utility Class
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}JWT Authentication Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}Authentication Controller
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(token));
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
// User registration logic
// Hash password with passwordEncoder.encode()
// Save user to database
// Generate token
// Return token
}
}6. OpenAPI/Swagger Documentation
Dependencies
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>Configuration
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("User Management API")
.version("1.0")
.description("REST API for managing users with Spring Boot")
.contact(new Contact()
.name("Chanh Le")
.email("contact@chanhle.dev")
.url("https://chanhle.dev")
)
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")
)
)
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}
}Controller Documentation
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
@Operation(
summary = "Get user by ID",
description = "Returns a single user by their ID"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "User found",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = UserDTO.class)
)
),
@ApiResponse(
responseCode = "404",
description = "User not found",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
),
@ApiResponse(responseCode = "401", description = "Unauthorized")
})
@SecurityRequirement(name = "bearerAuth")
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(
@Parameter(description = "User ID", required = true)
@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@Operation(
summary = "Create new user",
description = "Creates a new user and returns the created user"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "User created successfully"
),
@ApiResponse(
responseCode = "400",
description = "Invalid input"
)
})
@PostMapping
public ResponseEntity<UserDTO> createUser(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User object to be created",
required = true,
content = @Content(
schema = @Schema(implementation = CreateUserRequest.class)
)
)
@Valid @RequestBody CreateUserRequest request) {
UserDTO user = userService.createUser(request);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
}Access Swagger UI: http://localhost:8080/swagger-ui.html
7. Testing Strategies
Unit Tests
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void testGetUserById_Success() {
// Arrange
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setUsername("testuser");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
UserDTO result = userService.getUserById(userId);
// Assert
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("testuser", result.getUsername());
verify(userRepository, times(1)).findById(userId);
}
@Test
public void testGetUserById_NotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(ResourceNotFoundException.class, () -> {
userService.getUserById(userId);
});
}
}Integration Tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@Order(1)
public void testCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setUsername("testuser");
request.setEmail("test@example.com");
request.setPassword("Test@1234");
request.setAge(25);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("test@example.com"));
}
@Test
@Order(2)
public void testGetUserById() throws Exception {
mockMvc.perform(get("/api/v1/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").isNotEmpty());
}
@Test
public void testGetUserById_NotFound() throws Exception {
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("User not found with id: '999'"));
}
}Repository Tests
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void testFindByEmail_Success() {
// Arrange
User user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("password");
user.setAge(25);
userRepository.save(user);
// Act
Optional<User> found = userRepository.findByEmail("test@example.com");
// Assert
assertTrue(found.isPresent());
assertEquals("testuser", found.get().getUsername());
}
@Test
public void testExistsByEmail() {
// Arrange
User user = new User();
user.setEmail("test@example.com");
userRepository.save(user);
// Act
boolean exists = userRepository.existsByEmail("test@example.com");
// Assert
assertTrue(exists);
}
}Test Configuration
# src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true
jwt:
secret: testsecretkeytestsecretkeytestsecretkey
expiration: 36000008. Performance Optimization
Caching with Spring Cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "posts");
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public UserDTO getUserById(Long id) {
// Database call - cached after first call
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
// Invalidates cache on delete
}
@CachePut(value = "users", key = "#result.id")
public UserDTO updateUser(Long id, UpdateUserRequest request) {
// Updates cache with new value
}
}Database Query Optimization
// N+1 Query Problem - BAD
@OneToMany(mappedBy = "user")
private List<Post> posts;
// Solution 1: JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.posts WHERE u.id = :id")
User findByIdWithPosts(@Param("id") Long id);
// Solution 2: Entity Graph
@EntityGraph(attributePaths = {"posts", "comments"})
Optional<User> findById(Long id);
// Solution 3: Batch Fetching
@BatchSize(size = 10)
@OneToMany(mappedBy = "user")
private List<Post> posts;Connection Pool Tuning
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000Async Processing
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
public class EmailService {
@Async
public CompletableFuture<Void> sendWelcomeEmail(String email) {
// Send email asynchronously
return CompletableFuture.completedFuture(null);
}
}9. Production Best Practices
Health Checks and Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: trueLogging Configuration
logging:
level:
root: INFO
com.example.api: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file:
name: /var/log/app.log
max-size: 10MB
max-history: 30CORS Configuration
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}Rate Limiting
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>7.6.0</version>
</dependency>@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = request.getRemoteAddr();
Bucket bucket = resolveBucket(key);
if (bucket.tryConsume(1)) {
return true;
}
response.setStatus(429); // Too Many Requests
response.getWriter().write("Rate limit exceeded");
return false;
}
private Bucket resolveBucket(String key) {
return cache.computeIfAbsent(key, k -> {
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)));
return Bucket.builder()
.addLimit(limit)
.build();
});
}
}10. Complete Example Project Structure
src/
├── main/
│ ├── java/
│ │ └── com/example/api/
│ │ ├── config/
│ │ │ ├── SecurityConfig.java
│ │ │ ├── OpenAPIConfig.java
│ │ │ ├── CacheConfig.java
│ │ │ └── AsyncConfig.java
│ │ ├── controller/
│ │ │ ├── UserController.java
│ │ │ └── AuthController.java
│ │ ├── dto/
│ │ │ ├── UserDTO.java
│ │ │ ├── CreateUserRequest.java
│ │ │ ├── UpdateUserRequest.java
│ │ │ └── AuthResponse.java
│ │ ├── entity/
│ │ │ └── User.java
│ │ ├── exception/
│ │ │ ├── GlobalExceptionHandler.java
│ │ │ ├── ResourceNotFoundException.java
│ │ │ └── ErrorResponse.java
│ │ ├── filter/
│ │ │ └── JwtAuthenticationFilter.java
│ │ ├── repository/
│ │ │ └── UserRepository.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── UserServiceImpl.java
│ │ ├── specification/
│ │ │ └── UserSpecification.java
│ │ ├── util/
│ │ │ └── JwtUtil.java
│ │ └── ApiApplication.java
│ └── resources/
│ ├── application.yml
│ └── application-prod.yml
└── test/
├── java/
│ └── com/example/api/
│ ├── controller/
│ │ └── UserControllerTest.java
│ ├── service/
│ │ └── UserServiceTest.java
│ └── repository/
│ └── UserRepositoryTest.java
└── resources/
└── application-test.ymlSummary and Key Takeaways
In this comprehensive guide, we've covered advanced patterns for building production-ready REST APIs with Spring Boot:
✅ Exception Handling: Centralized error handling with @ControllerAdvice
✅ Validation: Bean Validation with custom validators
✅ Pagination: Spring Data pagination and filtering with Specifications
✅ API Versioning: URI, header, and content negotiation strategies
✅ Security: JWT authentication with Spring Security
✅ Documentation: OpenAPI/Swagger integration
✅ Testing: Unit, integration, and repository tests
✅ Performance: Caching, query optimization, async processing
✅ Production: Health checks, logging, CORS, rate limiting
Next Steps
- Implement these patterns in your existing Spring Boot projects
- Add monitoring with Prometheus and Grafana
- Deploy to production with Docker and Kubernetes
- Explore microservices architecture with Spring Cloud
Related Posts
- Getting Started with Spring Boot: Your First REST API
- Learning FastAPI: Advanced Patterns and Best Practices
- Docker for Backend Developers: A Practical Guide
Questions or feedback? Feel free to reach out at contact@chanhle.dev or connect with me on X.
Happy coding! 🚀
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.