Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ricpalomino/spring-boot/llms.txt

Use this file to discover all available pages before exploring further.

The Products API uses a single @ControllerAdvice class, GlobalExceptionHandler, to intercept every unhandled exception before it reaches the client. Instead of letting Spring’s default error page or a bare stack trace escape over the wire, GlobalExceptionHandler converts each exception into a structured ApiResponse JSON object with a machine-readable responseCode, a human-readable responseMessage, and an optional data payload. This means every error response your client receives has the same shape as a successful one.

ApiResponse envelope

All responses—success and error—are wrapped in the same ApiResponse<T> type:
@Setter
@Getter
public class ApiResponse<T> {

    private String responseCode;
    private String responseMessage;
    private T data;

    public ApiResponse(String responseCode, String responseMessage, T data) { ... }
    public ApiResponse(String responseCode, String responseMessage) { ... }
}
Error handlers use the two-argument constructor and leave data null, or pass a Map<String, String> of field-level errors for validation failures.

GlobalExceptionHandler

@ControllerAdvice
@Hidden
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleProductNotFoundException(ProductNotFoundException ex) {
        logger.warn("ProductNotFoundException: {}", ex.getMessage());
        ApiResponse<Void> response = new ApiResponse<>(String.valueOf(HttpStatus.NOT_FOUND.value()), ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        logger.warn("Validation error: {}", errors);
        ApiResponse<Map<String, String>> response = new ApiResponse<>(
            String.valueOf(HttpStatus.BAD_REQUEST.value()),
            "Error de validación",
            errors
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGeneralException(Exception ex) {
        logger.error("Unexpected error occurred", ex);
        ApiResponse<Void> response = new ApiResponse<>(
            String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()),
            "Ocurrió un error en el servidor"
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}
The @Hidden annotation prevents this class from appearing in the OpenAPI/Swagger UI output, keeping the API reference clean.

Exception handlers

ProductNotFoundException is thrown by the service layer whenever a requested product ID does not exist in the repository.
public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) {
        super(message);
    }
}
The handler logs a WARN-level message and returns HTTP 404 with the exception’s message as responseMessage:
{
  "responseCode": "404",
  "responseMessage": "Product not found",
  "data": null
}
When a controller method is annotated with @Valid and the incoming request body fails Jakarta Bean Validation, Spring throws MethodArgumentNotValidException. The handler extracts all field-level errors from BindingResult and places them in the data map so the client knows exactly which fields need correction.
{
  "responseCode": "400",
  "responseMessage": "Error de validación",
  "data": {
    "name": "El nombre del producto debe tener entre 3 y 100 caracteres"
  }
}
Multiple fields can fail simultaneously, and the data map will contain one entry per failing field.
Any exception not matched by the two handlers above falls through to the generic Exception handler. The full stack trace is logged at ERROR level (and written to logs/error.log by the dedicated error appender), but only a generic message is returned to the client to avoid leaking implementation details.
{
  "responseCode": "500",
  "responseMessage": "Ocurrió un error en el servidor",
  "data": null
}
A 500 response always means something unexpected happened in the server. Check logs/error.log immediately—the full stack trace is there. Do not expose internal exception messages to clients in production.

Response reference

HTTP statusresponseCodeTriggerdata
200 OK"200"Successful operationResource or list
404 Not Found"404"Product ID not foundnull
400 Bad Request"400"Bean Validation failureMap<field, message>
500 Internal Server Error"500"Any unhandled exceptionnull

Throwing ProductNotFoundException

Throw ProductNotFoundException from the service layer when a lookup by ID returns no result:
Product product = productRepository.findById(id)
    .orElseThrow(() -> new ProductNotFoundException("Product not found"));
GlobalExceptionHandler picks it up automatically. You do not need any try-catch in the controller.

Build docs developers (and LLMs) love