Week 4 - Rest APIs

Java Annotations

Introduction to REST

Spring Boot Setup

Writing Endpoints

Message Formats

Input Validation

Practice

Assignment

Back end Track

Introduction

So far, your Categories API accepts whatever the client sends, even a category with no name or a missing image.

Try POST /api/categories with empty image and name:

{
    "name": "",
    "image": ""
}

Currently it works. That is a problem.

In production, invalid data causes bugs downstream: corrupted databases, NullPointerException errors in business logic, and confused users. Validation checks data at the door, rejects what is wrong, and tells the client exactly what to fix. Spring Boot makes this declarative: you annotate fields, then Spring checks them when validation is triggered.

Prerequisites

Why validate input

The client can be anything: a browser, a mobile app, a script, a malicious actor. You cannot assume the data it sends is correct, complete, or safe.

Three reasons to validate:

  1. Data integrity: To prevent invalid data from entering the database. A category with an empty name or an image value of "!! NOT AN IMAGE !!" should never be stored.
  2. Security: To block injection attempts, oversized payloads such as a 10 MB image URL, or unexpected field values. Validation is one layer of defense.
  3. User experience: To return clear error messages so the client can fix their request. A generic "Bad Request" helps nobody; "Name is required" helps everyone.

<aside> 💡

Always validate on the server; optionally validate on the client too: Frontend validation is a UX convenience. It can be bypassed with curl, Postman, or browser dev tools. Server-side validation is the real security boundary.

</aside>

Frontend vs Backend Validation

Always validate on the server; optionally validate on the client too.

Like with DTOs and domain models, this is also a separation of concerns issue.

Validation on the frontend serves a different purpose. It improves the user experience by giving fast feedback before the request is sent. It can still be bypassed with curl, Postman, browser dev tools, or a custom script.

Server-side validation is connected to business logic and application safety. It protects your server from invalid input that could break functionality or support an attack. It is the real security boundary because the application runs on a server you control, not inside a client browser.

The same principle from Core Program applies here, except for never trust user input, which becomes never trust input. At the API level, anything can arrive:

Adding the validation dependency

Spring Boot's validation support comes from the spring-boot-starter-validation dependency. It is not included in the default Spring Web starter, so you need to add it explicitly.

Here is the Maven Central Repository page for it: https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-validation

Add the dependency:

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

<aside> 💡

In Spring Initializr, you can add Validation as a dependency during project creation. If you already have a project, add it manually to pom.xml and reload your Maven project in the IDE.

</aside>

If your project uses spring-boot-starter-parent, Spring Boot manages the dependency version for you. That keeps the validation starter compatible with the rest of your Spring Boot dependencies.

Under the hood, this starter pulls in Hibernate Validator, which implements the Jakarta Bean Validation specification.

You do not interact directly with Hibernate Validator in this chapter. Instead, you use standard validation annotations, such as @NotNull@Size, and @Email, from the jakarta.validation package. These annotations are part of the Jakarta Validation API included through the validation starter.

Bean Validation annotations

The jakarta.validation.constraints package provides annotations you place on fields or record components to declare validation rules. When Spring validates the object, it checks each annotated value.

Here are the most commonly used annotations:

Annotation What it checks Example use case
@NotNull Field is not null Required object references
@NotBlank String is not null, not empty, not only whitespace Required text fields (title, name)
@NotEmpty String/collection is not null and not empty Required lists
@Size(min=, max=) String length or collection size is within range Category name between 1–100 chars
@Min(value) Numeric value ≥ minimum Minimum allowed price on a product
@Max(value) Numeric value ≤ maximum Maximum allowed price on a product
@Email String matches email format Contact email fields
@Pattern(regexp=) String matches regex URL-safe slug, image URL
@Positive /
@PositiveOrZero Numeric value > 0 / ≥ 0 Price, quantity

<aside> ❗

@NotNull vs @NotBlank@NotNull only checks for null, so an empty string "" passes. For String fields, use @NotBlank to reject null, empty strings, and whitespace-only strings. This is one of the most common validation mistakes beginners make.

</aside>

<aside> 💡

In real-world projects, you often don’t write the validation annotations by hand. Just like with DTOs, they can be generated from OpenAPI specifications.

Still, if you know these annotations, you can do much more with your OpenAPI specifications because you know what to expect. Eg: If there is an @Email annotation, then very likely there is a way to declare that in your OpenAPI spec.

</aside>

Validation on the DTO

Imagine you already know a request is going to fail. You can’t make it succeed, but you can control exactly when it fails.

In such cases, failing as early as possible is the best approach from both a security and resource-efficiency perspective.

This makes DTOs the ideal place for our input validations because invalid input would be rejected before reaching our domain logic, services, or database.

💬 Can you think of a few other advantages of putting validations on the DTOs rather than on the domain models?

Validating Input for CreateCategoryRequest

Let’s apply some input validation to our CreateCategoryRequest DTO ( The Java record from previous chapter, it’s also shared in the pre-requisites section above ) .

For name and image , we can add the below :

public record CreateCategoryRequest(
    @NotBlank(message = "Name is required")
    @Size(max = 100, message = "Name must be 100 characters or fewer")
    String name,
    
    @NotBlank(message = "Image is required")
    @Pattern(regexp = "^https?://.*", message = "Image must be a valid http(s) URL")
    String image) {}

If you test whether the above annotations work on our server by making a bad request from Postman, you’ll notice that the request still succeeds.

image.png

This is because, if you recall from Java Annotations chapter, these annotations have NO effect by themselves. They are just metadata. Spring needs to be told to check them and that's where @Valid comes into picture.

Triggering validation with @Valid

@Valid tells Spring: "Before calling this method, validate the object using its Bean Validation annotations." If validation fails, Spring does not call the method. It throws MethodArgumentNotValidException instead.

Add it to the request body parameter for createCategory:

  @PostMapping
  public ResponseEntity<Category> createCategory(@Valid @RequestBody CreateCategoryRequest request) {
		long nextId = categories.stream()
		.map(Category::getId)
		.max(Comparator.naturalOrder())
		.orElse(0L) + 1;
		
		Category category = new Category(nextId, request.name(), request.image());
		categories.add(category);
		
		URI location = URI.create("/api/v1/categories/" + nextId);
		return ResponseEntity.created(location).body(category);
  }

Now the request correctly fails and it also has the right HTTP status:

image.png

However, as you can see, the message we passed to @NotBlank(message = "Name is required") is not there in the error response.

A DTO can have multiple invalid fields, so Spring cannot always guess the response shape you want. You have to handle this yourself, which we’ll cover shortly.

Default validation failure behavior

When validation fails, Spring automatically returns HTTP status 400 Bad Request which is the correct status code for invalid input.

But the default response body is a generic Spring error object with a timestamp, status, and error type. Field-level error messages may be buried or missing, depending on configuration. That is not useful for clients trying to fix their request.

We need to fix this so the client sees exactly which fields failed and why.

Custom Error Responses

This is the most important practical section of this chapter. You’ll intercept validation exceptions and return clean, client-friendly error messages.

@ExceptionHandler

@ExceptionHandler is a method-level annotation that tells Spring:

When this specific exception type is thrown? Call this method instead of the default error handler.

It is the same concept as try/catch, but at the framework level.

Important Note on @ExceptionHandler Methods

@ExceptionHandler methods don’t replace try/catch blocks.