Pular para o conteúdo principal

Capítulo 3: Gestão de Erros HTTP

Introdução​

Uma API REST profissional deve gerenciar erros de forma clara e consistente, retornando os códigos de status HTTP corretos e mensagens de erro explícitas.


ResponseEntity​

O que é ResponseEntity?​

ResponseEntity é uma classe do Spring que permite controlar completamente a resposta HTTP:

  • Código de status HTTP
  • Cabeçalhos HTTP
  • Corpo da resposta

Sintaxe Básica​

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

Principais Códigos de Status HTTP​

Tabela Resumo​

CodeNomeUsoExemplo
200OKSucesso geralGET bem-sucedido
201CreatedRecurso criadoPOST bem-sucedido
204No ContentSucesso sem conteúdoDELETE bem-sucedido
400Bad RequestRequisição inválidaValidação falhou
404Not FoundRecurso não encontradoGET id inexistente
500Internal Server ErrorErro no servidorExceção inesperada

Exemplos Práticos com ResponseEntity​

200 OK - Recuperação Bem-sucedida​

@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 - Criação Bem-sucedida​

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

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

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

204 No Content - Remoção Bem-sucedida​

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

404 Not Found - Recurso Não 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();
}
}

Gestão Global de Exceções com @ControllerAdvice​

Exceção Personalizada​

package com.example.demo.exception;

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

Handler 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);
}
}

Utilização no Service​

@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));
}
}

Resposta JSON​

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

Classe 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 e 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;
}
}

Utilização​

@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);
}
}

Arquitetura Completa: Controller + Service + Exception​

1. Controller​

@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. Service​

@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("O preço não pode 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. Exceções 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. Handler 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);
}
}

Boas Práticas​

DO (O que fazer)​

✅ Usar ResponseEntity para todas as respostas
✅ Retornar os códigos HTTP corretos
✅ Criar exceções personalizadas
✅ Utilizar @ControllerAdvice para gestão global
✅ Fornecer mensagens de erro claras

DON'T (O que evitar)​

❌ Sempre retornar 200 OK
❌ Expor rastros de erro técnicos ao cliente
❌ Tratar exceções em cada controller
❌ Usar Exception genérica


Resumo do Capítulo​

Pontos-chave​

  • ResponseEntity para controlar as respostas HTTP
  • Códigos de status apropriados (200, 201, 204, 400, 404, 500)
  • @ControllerAdvice para gestão global de erros
  • Exceções personalizadas para clareza do código
  • ErrorResponse para estrutura consistente

Próximo capítulo: Arquitetura 3 Camadas

Voltar ao Módulo 3