Spring Boot
404 error
custom error response
REST API
error handling

Spring boot 404 error custom error response ReST

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

A REST API should return a structured JSON response for 404 errors instead of an HTML error page. Clients, gateways, and monitoring tools all depend on a predictable error contract. In Spring Boot, the main design choice is to separate two different 404 cases: a request for a route that does not exist, and a request for a real route whose target resource was not found.

Distinguish Route 404 From Resource 404

These two cases use the same HTTP status code but mean different things.

  • route 404: no controller mapping matches the request path
  • resource 404: the controller exists, but the requested entity does not

That distinction matters because API clients often need different messages or handling for each one.

Handle Missing Domain Resources Explicitly

For domain-level not-found cases, define your own exception and handle it in @RestControllerAdvice.

java
1public class UserNotFoundException extends RuntimeException {
2    public UserNotFoundException(String id) {
3        super("User not found: " + id);
4    }
5}
java
1import org.springframework.http.HttpStatus;
2import org.springframework.http.ResponseEntity;
3import org.springframework.web.bind.annotation.ExceptionHandler;
4import org.springframework.web.bind.annotation.RestControllerAdvice;
5
6import java.util.Map;
7
8@RestControllerAdvice
9public class ApiErrorHandler {
10
11    @ExceptionHandler(UserNotFoundException.class)
12    public ResponseEntity<Map<String, Object>> handleUserNotFound(UserNotFoundException ex) {
13        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of(
14            "status", 404,
15            "error", "User not found",
16            "message", ex.getMessage()
17        ));
18    }
19}

This makes resource-level errors explicit and easy to test.

Handle Unknown Routes as JSON Too

Unknown routes are different because the request never reaches your controller. For that case, configure Spring MVC to throw an exception for missing handlers.

properties
spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false

Then handle NoHandlerFoundException in the same advice layer.

java
1import org.springframework.http.HttpStatus;
2import org.springframework.http.ResponseEntity;
3import org.springframework.web.bind.annotation.ExceptionHandler;
4import org.springframework.web.bind.annotation.RestControllerAdvice;
5import org.springframework.web.servlet.NoHandlerFoundException;
6
7import java.time.Instant;
8import java.util.Map;
9
10@RestControllerAdvice
11public class RouteErrorHandler {
12
13    @ExceptionHandler(NoHandlerFoundException.class)
14    public ResponseEntity<Map<String, Object>> handleNoHandler(NoHandlerFoundException ex) {
15        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of(
16            "timestamp", Instant.now().toString(),
17            "status", 404,
18            "error", "Not Found",
19            "path", ex.getRequestURL(),
20            "message", "No matching route"
21        ));
22    }
23}

Now unmapped endpoints return JSON instead of a default HTML response.

ProblemDetail Is a Good Modern Option

If you are on a newer Spring stack, ProblemDetail is a good standard format.

java
1import org.springframework.http.HttpStatus;
2import org.springframework.http.ProblemDetail;
3import org.springframework.web.bind.annotation.ExceptionHandler;
4import org.springframework.web.bind.annotation.RestControllerAdvice;
5import org.springframework.web.servlet.NoHandlerFoundException;
6
7@RestControllerAdvice
8public class ProblemDetailHandler {
9
10    @ExceptionHandler(NoHandlerFoundException.class)
11    public ProblemDetail routeNotFound(NoHandlerFoundException ex) {
12        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
13        pd.setTitle("Route not found");
14        pd.setDetail("No handler for " + ex.getHttpMethod() + " " + ex.getRequestURL());
15        return pd;
16    }
17}

The exact schema is less important than consistency across the whole API surface.

Test the Contract End to End

Custom error handling is part of the API contract, so test it with MockMvc or the equivalent HTTP-level tooling.

java
1import org.junit.jupiter.api.Test;
2import org.springframework.beans.factory.annotation.Autowired;
3import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
4import org.springframework.boot.test.context.SpringBootTest;
5import org.springframework.test.web.servlet.MockMvc;
6
7import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
9import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
10
11@SpringBootTest
12@AutoConfigureMockMvc
13class NotFoundContractTest {
14
15    @Autowired
16    MockMvc mockMvc;
17
18    @Test
19    void unknownRouteReturnsJson404() throws Exception {
20        mockMvc.perform(get("/unknown-route"))
21                .andExpect(status().isNotFound())
22                .andExpect(jsonPath("$.status").value(404));
23    }
24}

That keeps future framework upgrades from silently changing your 404 payload shape.

Common Pitfalls

  • Handling missing resources but forgetting unmapped routes.
  • Returning HTML for some 404 cases and JSON for others.
  • Using one generic message for every not-found scenario.
  • Skipping tests for the error payload contract.
  • Forgetting that gateways or proxies may rewrite error responses upstream.

Summary

  • A REST API should return structured JSON for 404 responses.
  • Route-level 404 and resource-level 404 are different cases.
  • Use @RestControllerAdvice to centralize the error contract.
  • Enable NoHandlerFoundException handling if you want JSON for unknown routes.
  • Keep the payload shape consistent so clients can rely on it.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.