Estrutura de Resposta Unificada
Ao construir APIs RESTful, garantir que todas as respostas sigam um formato previsível melhora significativamente a integração com clientes front-end e outros microsserviços. O primeiro passo é definir um enumerador para padronizar os códigos e mensagens de status da aplicação.
Enumerador de Códigos de Status
public enum ApiStatusCode {
OK(200, "Operação concluída com sucesso"),
BAD_REQUEST(400, "Requisição inválida"),
VALIDATION_ERROR(1001, "Dados de entrada inválidos"),
RESOURCE_NOT_FOUND(1002, "Recurso não localizado"),
INTERNAL_FAILURE(9999, "Erro interno inesperado");
private final int status;
private final String description;
ApiStatusCode(int status, String description) {
this.status = status;
this.description = description;
}
public int getStatus() {
return status;
}
public String getDescription() {
return description;
}
}
Classe de Envelope de Resposta
Em seguida, criamos uma classe genérica que atuará como um envelope para todos os dados retornados pelos controladores. Utilizamos métodos estáticos de fábrica para simplificar a criação de respostas de sucesso e falha.
public class Payload<T> {
private int status;
private String message;
private T body;
private Payload(int status, String message, T body) {
this.status = status;
this.message = message;
this.body = body;
}
public static <T> Payload<T> success(T data) {
return new Payload<>(ApiStatusCode.OK.getStatus(), ApiStatusCode.OK.getDescription(), data);
}
public static <T> Payload<T> failure(ApiStatusCode code) {
return new Payload<>(code.getStatus(), code.getDescription(), null);
}
public int getStatus() { return status; }
public String getMessage() { return message; }
public T getBody() { return body; }
}
Exemplo de Uso no Controlador
@RestController
@RequestMapping("/api/v1/items")
public class ItemController {
@GetMapping("/count")
public Payload<Integer> getTotalItems() {
int total = 42;
return Payload.success(total);
}
}
Tratamento Centralizado de Exceções
Para evitar que erros não tratados retornem stack traces ou formatos HTML padrão do servidor, implementamos um menipulador global. Isso garante que até mesmo falhas inesperadas ou regras de negócio violadas sejam empacotadas no nosso formato padrão.
Exceção de Negócio Customizada
Definimos uma exceção específica para regras de domínio que carrega o nosso enumerador de status.
public class BusinessException extends RuntimeException {
private final ApiStatusCode statusCode;
public BusinessException(ApiStatusCode statusCode) {
super(statusCode.getDescription());
this.statusCode = statusCode;
}
public ApiStatusCode getStatusCode() {
return statusCode;
}
}
Manipulador Global de Exceções
Utilizamos a anotação @RestControllerAdvice para interceptar exceções lançadas em qualquer controlador e convertê-las em uma resposta esrtuturada, definindo também o HTTP Status Code correto para a resposta.
@RestControllerAdvice
public class GlobalApiExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Payload<Void>> handleBusinessException(BusinessException ex) {
Payload<Void> response = Payload.failure(ex.getStatusCode());
HttpStatus httpStatus = HttpStatus.valueOf(ex.getStatusCode().getStatus());
return new ResponseEntity<>(response, httpStatus);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Payload<Void>> handleGenericException(Exception ex) {
Payload<Void> response = Payload.failure(ApiStatusCode.INTERNAL_FAILURE);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Simulação de Lançamento de Exceção
@RestController
@RequestMapping("/api/v1/users")
public class UserEndpoint {
@GetMapping("/profile/{id}")
public Payload<String> fetchProfile(@PathVariable Long id) {
if (id <= 0) {
throw new BusinessException(ApiStatusCode.VALIDATION_ERROR);
}
if (id == 999) {
throw new BusinessException(ApiStatusCode.RESOURCE_NOT_FOUND);
}
return Payload.success("User data for ID: " + id);
}
}