JAVA + SPRING AI — ADVANCED
Overriding Auto-Configuration, REST Error Handling & Validation in Spring Boot
The Intermediate level covered how a working @RestControllergets built on top of Spring Boot’s auto-configuration. Here’s what a production-ready version of that same controller needs: the ability to override a default when it doesn’t fit, a real strategy for errors, and protection against malformed input.
The Quick Answer
You can override almost any auto-configured default simply by defining your own @Bean of that same type — because the relevant auto-configuration class is typically gated by @ConditionalOnMissingBean, it simply backs off once your bean already exists. For errors, Spring’s @ExceptionHandler and @RestControllerAdvice let you centralize how exceptions become HTTP responses, using ResponseEntity to return the right status code and body together. And for bad input, Bean Validation annotations like @NotNull and @Size, combined with @Valid, cause Spring to throw a MethodArgumentNotValidException the moment a request body fails validation, before your own handler code even runs.
Overriding an Auto-Configured Default
Spring Boot’s own documentation describes auto-configuration as “non-invasive” — its own words are that “at any point, you can start to define your own configuration to replace specific parts of the auto-configuration.” The mechanism behind that, covered at the Intermediate level, is @ConditionalOnMissingBean: most auto-configuration classes only create their default bean if you haven’t already supplied one of that same type yourself. Define your own, and the auto-configured one is never created at all — your bean is simply the one the rest of the application receives.
A concrete example — Spring Boot auto-configures a default ObjectMapper (the Jackson class actually responsible for the JSON conversion covered at the Intermediate level). If your application needs different JSON behavior, defining your own ObjectMapper @Bean is enough — no need to disable or fight the auto-configuration, it simply steps aside:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}From this point forward, every part of the application that depends on an ObjectMapper — including the machinery serializing your @RestController responses — gets this exact instance, not Spring Boot’s default.
Centralized Error Handling
Left alone, an unhandled exception thrown from inside a controller method produces Spring Boot’s own default error response — usable, but generic. For a real API, you typically want to control the exact shape of that error response yourself. @ExceptionHandleris the annotation that marks a method as the handler for a specific exception type, documented as part of Spring MVC’s regular annotated-controller support. Placing those methods inside a class annotated @RestControllerAdvice — Spring’s own documentation describes it as “a shortcut annotation that combines @ControllerAdvice with @ResponseBody” — lets that error-handling logic apply across every controller in the application, in one place, instead of duplicating the same handler method inside every controller class individually.
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book " + id + " not found");
}
}
public record ErrorResponse(int status, String message) {}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(BookNotFoundException ex) {
ErrorResponse body = new ErrorResponse(404, ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
}ResponseEntity is what makes returning a specific status code alongside a body possible in one return value — without it, a handler method could return the error body, but not directly control the HTTP status code that goes with it. Now, every time bookService.findById(id) throws BookNotFoundException from the Intermediate level’s controller, this one handler produces a consistent, real 404 response — instead of a generic 500 error or Spring Boot’s default error page.
Validating Request Bodies
A controller that trusts every incoming request body blindly is asking for trouble — a client can send a book with no title, or a wildly oversized field, and without validation, that bad data flows straight into your service layer. Bean Validation annotations, placed directly on a request DTO, describe the rules a valid object must satisfy. One version detail worth being explicit about: in current Spring Boot (the Spring Framework 7.x / Spring Boot 4.x this pillar uses), @NotNull, @Size, and @Valid are imported from the jakarta.validation.* package, not the older javax.validation.* package these same annotations lived in prior to Spring Boot 3:
public record NewBookRequest(
@NotNull(message = "title is required")
@Size(min = 1, max = 200, message = "title must be 1-200 characters")
String title,
@NotNull(message = "author is required")
String author
) {}Those annotations do nothing on their own — validation only actually runs when the parameter is also marked @Valid:
private final AtomicLong idCounter = new AtomicLong();
private Long nextId() {
return idCounter.incrementAndGet();
}
@PostMapping
public Book createBook(@Valid @RequestBody NewBookRequest request) {
return bookService.save(new Book(nextId(), request.title(), request.author()));
}NewBookRequest deliberately has no idfield — a client creating a new book shouldn’t be the one supplying its identity, so this controller generates one itself with a simple AtomicLong counter before handing the fully-formed Book to bookService.save(...). (A real application would typically let the database assign this instead, but the principle — the caller doesn’t choose the id — is the same one.)
When a request fails one of these rules, Spring throws a MethodArgumentNotValidException before createBook’s own body ever runs — the invalid object never even reaches your code. Spring Boot’s default behavior already turns this into an HTTP 400 Bad Request response; adding a dedicated @ExceptionHandler for MethodArgumentNotValidException inside the same @RestControllerAdvice class above is how you’d customize that response further — for example, listing exactly which fields failed and why, rather than returning Spring Boot’s default message.
Where This Goes Next
Step back, and everything across all three levels of this topic — auto-configuration, @RestController, mappings, error handling, validation — adds up to one working, production-shaped REST API. That’s not a coincidence: this same REST controller pattern is exactly what a single microservice exposes to the outside world. The next topic in this pillar looks at what happens once you have more than one of these talking to each other.
Fun Fact
@RestControllerAdvice follows the exact same “shortcut” shape as @RestController itself from the Intermediate level — both are just their plain counterpart (@ControllerAdvice and @Controller, respectively) pre-combined with @ResponseBody, so nothing you return needs to be treated as an HTML page name.
Test Yourself
If a developer defines their own @Bean of a type Spring Boot would otherwise auto-configure, what happens?
What does @RestControllerAdvice let you do?
What is ResponseEntity used for in a REST error handler?
What exception does Spring throw when @Valid validation fails on a @RequestBody parameter?
How does the REST controller built in this topic connect to the next topic in this pillar?