Building Robust APIs with Spring Boot and Validation Annotations

When building APIs, the hardest bugs often come from the easiest oversight: trusting user input. If your controller accepts data as-is and only relies on business logic to catch errors, you’re putting too much responsibility on the wrong layer.

Validation isn’t a nice-to-have. It’s a contract.

In this post, you’ll learn how to build clean and resilient APIs using Spring Boot’s validation annotations — like @Valid, @NotNull, @Size, and how to write your own custom validators — to enforce data integrity at the boundary of your application.

Why Validation Matters at the API Layer

An API is more than an endpoint — it’s a promise. Whether it’s a public-facing interface or an internal service-to-service call, accepting malformed input can lead to:

  • Downstream failures
  • Data corruption
  • Business logic complexity
  • Security vulnerabilities

The controller layer is the gatekeeper. And validation is your first line of defense.

Enabling Validation in Spring Boot

Spring Boot integrates JSR 380 (Bean Validation 2.0) via Hibernate Validator by default.

First, ensure your project includes:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Basic Validation with @Valid, @NotNull, @Size

Let’s start with a classic example: a user registration API.

public class RegisterUserRequest {

  @NotNull(message = "Username is required")
  @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
  private String username;

  @NotNull(message = "Email is required")
  @Email(message = "Email should be valid")
  private String email;

  @NotNull(message = "Password is required")
  @Size(min = 8, message = "Password must be at least 8 characters")
  private String password;

  // getters and setters
}

In the controller:

@RestController
@RequestMapping("/api/users")
public class UserController {

  @PostMapping("/register")
  public ResponseEntity<String> register(@Valid @RequestBody RegisterUserRequest request) {
    // If validation fails, this method won’t even be called
    return ResponseEntity.ok("User registered successfully");
  }
}

Handling Validation Errors Gracefully

By default, Spring throws a MethodArgumentNotValidException. Let’s customize the response:

@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();

    ex.getBindingResult().getFieldErrors().forEach(error ->
      errors.put(error.getField(), error.getDefaultMessage())
    );

    return ResponseEntity.badRequest().body(errors);
  }
}

Now the client receives a structured response like:

{
  "username": "Username must be between 3 and 20 characters",
  "email": "Email should be valid"
}
Postman Screenshot of Validation Result

Nested Object Validation with @Valid

Validation isn’t just for flat structures.

public class CreateOrderRequest {

  @NotNull
  @Valid
  private Address shippingAddress;

  @NotEmpty
  @Valid
  private List<OrderItem> items;

  // getters and setters
}

With @Valid, Spring recursively validates all nested fields.

Creating a Custom Validator: The Case for @StrongPassword

Let’s say you want to ensure passwords meet custom rules: at least one uppercase, one lowercase, and one number.

1. Create the Annotation

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = StrongPasswordValidator.class)
public @interface StrongPassword {
  String message() default "Password must contain uppercase, lowercase, and a digit";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

2. Create the Validator

public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {

  private static final Pattern PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$");

  @Override
  public boolean isValid(String password, ConstraintValidatorContext context) {
    return password != null && PATTERN.matcher(password).matches();
  }
}

Now you can annotate:

@StrongPassword
private String password;

Validation Cheat-Sheet

Spring Boot Validation Cheat-Sheet

Final Thoughts

Input validation is not just about avoiding garbage — it’s about building confidence in your APIs. When you annotate your DTOs and use @Valid, you formalize the contract between the client and the server.

Here’s what you get out of the box:

  • Immediate feedback to clients
  • Cleaner controller logic
  • Centralized and consistent error handling
  • Protection against malformed or malicious inputs

Start treating your DTOs as the spec, not the fallback.


Want to go deeper?

If you liked this post and want more advanced content on API development, security, and real-world backend practices, follow me or check out the free eBooks below.

Leave a Reply

Your email address will not be published. Required fields are marked *