Saltar al contenido principal

Capítulo 3: Gestión de Errores HTTP

Introducción​

Una API REST profesional debe gestionar los errores de manera clara y coherente, devolviendo los códigos de estado HTTP correctos y mensajes de error explícitos.


ResponseEntity​

¿Qué es ResponseEntity?​

ResponseEntity es una clase de Spring que permite controlar completamente la respuesta HTTP:

  • Código de estado HTTP
  • Encabezados HTTP
  • Cuerpo de la respuesta

Sintaxis Básica​

ResponseEntity<TypeRetour> methodName() {
return ResponseEntity
.status(HttpStatus.OK)
.body(data);
}

Códigos de Estado HTTP Principales​

Tabla Resumen​

CodeNombreUsoEjemplo
200OKÉxito generalGET exitoso
201CreatedRecurso creadoPOST exitoso
204No ContentÉxito sin contenidoDELETE exitoso
400Bad RequestSolicitud inválidaValidación fallida
404Not FoundRecurso no encontradoGET id inexistente
500Internal Server ErrorError del servidorExcepción inesperada

Ejemplos Prácticos con ResponseEntity​

200 OK - Recuperación Exitosa​

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}

// Equivalente a:
return ResponseEntity.status(HttpStatus.OK).body(product);

201 Created - Creación Exitosa​

@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

// O con URI:
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();

return ResponseEntity.created(location).body(created);

204 No Content - Eliminación Exitosa​

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}

404 Not Found - Recurso No Encontrado​

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
try {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
} catch (ProductNotFoundException e) {
return ResponseEntity.notFound().build();
}
}

Gestión Global de Excepciones con @ControllerAdvice​

Excepción Personalizada​

package com.example.demo.exception;

public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}

Manejador Global​

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {

Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.NOT_FOUND.value());
body.put("error", "Not Found");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false).replace("uri=", ""));

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGlobalException(
Exception ex, WebRequest request) {

Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false).replace("uri=", ""));

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}

Uso en el Servicio​

@Service
public class ProductService {

private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
}

Respuesta JSON​

{
"timestamp": "2025-11-28T10:30:45",
"status": 404,
"error": "Not Found",
"message": "Product not found with id: 123",
"path": "/api/products/123"
}

Clase ErrorResponse Personalizada​

package com.example.demo.exception;

import java.time.LocalDateTime;

public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;

public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}

// Getters y setters
public LocalDateTime getTimestamp() {
return timestamp;
}

public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public String getError() {
return error;
}

public void setError(String error) {
this.error = error;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}
}

Uso​

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {

ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

Arquitectura Completa: Controller + Service + Exception​

1. Controlador​

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}

@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}

2. Servicio​

@Service
public class ProductService {

private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}

public Product createProduct(Product product) {
if (product.getPrice() < 0) {
throw new InvalidProductException("El precio no puede ser negativo");
}
return productRepository.save(product);
}

public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}

public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}

3. Excepciones Personalizadas​

public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}

public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}

4. Manejador Global​

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(404, "Not Found", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(InvalidProductException.class)
public ResponseEntity<ErrorResponse> handleInvalidProduct(
InvalidProductException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(400, "Bad Request", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(500, "Internal Server Error", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}

Buenas Prácticas​

DO (A Hacer)​

✅ Usar ResponseEntity para todas las respuestas
✅ Devolver los códigos HTTP correctos
✅ Crear excepciones personalizadas
✅ Utilizar @ControllerAdvice para la gestión global
✅ Proporcionar mensajes de error claros

DON'T (A Evitar)​

❌ Devolver siempre 200 OK
❌ Exponer trazas de error técnicas al cliente
❌ Gestionar las excepciones en cada controlador
❌ Usar Exception genérica


Resumen del Capítulo​

Puntos Clave​

  • ResponseEntity para controlar las respuestas HTTP
  • Códigos de estado apropiados (200, 201, 204, 400, 404, 500)
  • @ControllerAdvice para gestión global de errores
  • Excepciones personalizadas para claridad del código
  • ErrorResponse para estructura coherente

Capítulo siguiente: Arquitectura de 3 Capas

Volver al Módulo 3