Sections in this lesson

REST, Validation & Errors

Define bounded request DTOs, stable responses and safe error behavior at an HTTP boundary.

Reviewed: · Content owner: Interview Preparator

What you will learn

  • Validate request structure before service execution.
  • Separate invalid input from model/provider failure.
  • Derive authorization context from authenticated identity.

Before you start

The flow

  1. 1.Authenticate
  2. 2.Validate DTO
  3. 3.Resolve tenant
  4. 4.Call service
  5. 5.Map result

A DTO is a boundary contract

The Ask record accepts one bounded non-blank question. @Valid activates Bean Validation at the controller boundary; it does not prove that a question is truthful or safe. Keep transport validation and domain constraints distinct.

Returning an ANSWER or ABSTAIN status is normal application behavior. An unsupported question should not be treated as an internal exception or converted into a fabricated answer.

Identity is not a DTO field

The controller maps a trusted principal to a tenant. A request body or generated tool argument must not select its own tenant permissions. In a real service, replace the demo mapping with an authenticated identity/tenant policy.

Use explicit response DTOs and stable error codes. Invalid model output becomes a sanitized 502 error in this lab. Do not echo raw provider responses, SQL details or credentials in error bodies.

Verify observable behavior

The integration test exercises the real MVC validation, security filter chain and database-backed service. It checks unauthenticated access, a POST without CSRF, blank input and a successful request. A controller unit test alone cannot prove those components are connected.

Try it

TutorController.java
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.interviewpreparator.lab;
import java.security.Principal;
import java.util.Map;
import java.util.List;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TutorController {
record Ask(@NotBlank @Size(max = 500) String question,
@Size(max = 10) List<@NotNull @Pattern(regexp = "[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}") String> sourceIds) {}
private final RagService service;
public TutorController(RagService service) { this.service = service; }
@GetMapping("/api/csrf")
Map<String, String> csrf(CsrfToken token) {
return Map.of("headerName", token.getHeaderName(), "token", token.getToken());
}
@PostMapping("/api/tutor/ask")
RagService.Answer ask(@Valid @RequestBody Ask request, Principal principal) {
String tenant = switch (principal.getName()) {
case "alpha" -> "tenant-a";
case "beta" -> "tenant-b";
default -> throw new IllegalArgumentException("Unmapped principal");
};
return service.answer(tenant, request.question(), request.sourceIds());
}
@ExceptionHandler(RagService.InvalidModelOutput.class)
@ResponseStatus(HttpStatus.BAD_GATEWAY)
Map<String, String> invalidModelOutput() { return Map.of("error", "MODEL_RESPONSE_INVALID"); }
}

examples/spring-ai-lab/src/main/java/com/interviewpreparator/lab/TutorController.java · mvn -f examples/spring-ai-lab/pom.xml -Pintegration verify. Java 21+, Spring Boot 4.0.5, Spring AI 2.0.2 where applicable. The demo uses synthetic models; live inference quality is a separate check.

Interview questions

Is @Valid sufficient for authorization?

No. It validates declared constraints; ownership is a separate authenticated resource policy.

Should missing evidence be HTTP 500?

Usually no. Represent a known unsupported/abstain outcome explicitly and reserve failures for actual errors.

Why sanitize provider errors?

Raw messages can leak prompts, credentials or implementation details and make the public contract unstable.

Practice

Add an oversized HTTP request case and a documented error response for validation failures.

Check your solution

  • Blank and oversized input never trigger model calls.
  • The response contract distinguishes abstention and failure.
  • Tenant comes from the principal.

Official references

Continue learning

Category sequence

Spring & Spring Boot

Lesson 3 / 4

Was this lesson useful?

Change notes: Phase 2: bilingual lesson with versioned executable reference.