Error Handling and Validation Done Right
Error Handling and Validation Done Right
A good API fails loudly and helpfully. Spring gives you two mechanisms that belong together: bean validation for input, and exception handlers that turn failures into consistent JSON responses.
Declarative validation
Annotate request DTO fields with JSR-380 constraints like @NotBlank, @Email, and @Size, then add @Valid to the parameter. Spring runs the constraints automatically and rejects invalid payloads before your method body ever runs.
public record CreateProductRequest(
@NotBlank String name,
@Positive double price) {}
@PostMapping
public Product create(@Valid @RequestBody CreateProductRequest req) {
return service.create(req);
}
Global error handling
When validation fails, Spring throws MethodArgumentNotValidException with a default response that many clients find verbose. Centralize your error shape with @RestControllerAdvice, which intercepts exceptions thrown across all controllers and returns a uniform body with a timestamp, status, and message.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, Object> notFound(NotFoundException ex) {
return Map.of("error", "not_found",
"message", ex.getMessage());
}
}
Custom exceptions
Create small exception classes like NotFoundException and throw them from services. A handler per exception type keeps mapping logic out of controllers and gives you one place to set status codes and log details.
Validation messages and nesting
Set readable messages on constraints and consider @Valid on nested objects so deep DTOs are validated fully. Return field-level errors by mapping the binding result into a list of field and message pairs, which many frontends prefer.
Key Points
- Use JSR-380 constraints plus
@Validfor declarative input validation. - Centralize error responses with
@RestControllerAdviceand@ExceptionHandler. - Throw custom exceptions from services and map them to HTTP statuses in one place.
- Return a stable error shape with status codes and readable messages.
- Validate nested DTOs with
@Validon composed fields.