API RESTful: Um Guia para Retornos Elegantes e Padronizados

No cenário atual de desenvolvimento de aplicações web e móveis, especialmente com a ascensão das arquiteturas de microsserviços e a separação entre front-end e back-end, a forma como as APIs retornam dados é crucial para a eficiência e clareza. Uma API bem estruturada facilita a integração e o desenvolvimento, tanto para equipes de front-end quanto de back-end.

Estrutura de Retorno da API

A comunicação entre o front-end e o back-end geralmente ocorre através de requisições HTTP, onde o front-end solicita um recurso em um determinado URL e envia parâmetros. O back-end processa a requisição e retorna os dados. A forma mais comum de retornar dados é utilizando o formato JSON, seguindo uma estrutura padronizada:


   {
       "codigo": integer,
       "mensagem": string,
       "dados": object
   }
   

Código de Status Padronizado

O campo codigo é fundamental para indicar o resultado da operação. Em vez de definir códigos arbitrários para cada situação (como 101 para erro de permissão, 102 para erro de parâmetro), é mais robusto seguir uma nomenclatura inspirada nos códigos de status HTTP, mas expandida para abranger cenários específicos da aplicação. Isso permite uma categorização clara dos erros:

  • 2xx: Sucesso na requisição.
  • 3xx: Redirecionamento ou recursos movidos.
  • 4xx: Erros do cliente (ex: requisição inválida, não autorizado).
  • 5xx: Erros do servidor (ex: erro interno).

Podemos definir intervalos para códigos personalizados, por exemplo:

  • 1000-1999: Erros relacionados a parâmetros.
  • 2000-2999: Erros relacionados ao usuário ou autenticação.
  • 3000-3999: Erros específicos da lógica da API ou do serviço.

Essa padronização ajuda a equipe de front-end a entender rapidamente a natureza do problema com base no código e na mensagem de retorno.

Mensagem Descritiva

O campo mensagem fornece uma descrição legível por humanos sobre o resultado da operação, seja um sucesso ou um erro. Ao combinar o codigo com uma mensagem clara, a depuração se torna mais ágil.

Dados da Resposta

O campo dados contém o payload da resposta, que varia de acordo com a requisição. Para facilitar o manuseio desses retornos em toda a aplicação, podemos criar uma classe de encapsulamento, como Resultado ou ApiResponse.


   public class ApiResponse<T> {
       private int codigo;
       private String mensagem;
       private T dados;

       // Construtores, getters e setters
   }
   

Otimizando o Controller

No controller, ao retornar os dados, podemos envolver o objeto de negócio em nossa classe de retorno:


   @GetMapping("/pedido/{id}")
   public ApiResponse<Pedido> obterPedido(@PathVariable Long id) {
       Pedido pedido = servicoPedido.buscarPorId(id);
       if (pedido != null) {
           return new ApiResponse<>(200, "Sucesso", pedido);
       } else {
           return new ApiResponse<>(404, "Pedido não encontrado", null);
       }
   }
   

Para tornar o código mais conciso, podemos adicionar métodos estáticos à classe ApiResponse:


   public class ApiResponse<T> {
       // ... outros campos e construtores

       public static <T> ApiResponse<T> sucesso(T dados) {
           return new ApiResponse<>(200, "Operação bem-sucedida", dados);
       }

       public static <T> ApiResponse<T> falha(int codigo, String mensagem) {
           return new ApiResponse<>(codigo, mensagem, null);
       }
   }
   

E refatorar o controller:


   @GetMapping("/pedido/{id}")
   public ApiResponse<Pedido> obterPedido(@PathVariable Long id) {
       Pedido pedido = servicoPedido.buscarPorId(id);
       return pedido != null ? ApiResponse.sucesso(pedido) : ApiResponse.falha(404, "Pedido não encontrado");
   }
   

Abordagem Elegante com AOP

Embora a abordagem com métodos estáticos seja mais limpa, ela ainda exige que os controllers retornem explicitamente um objeto ApiResponse. Uma solução mais elegante é utilizar Aspect-Oriented Programming (AOP) para interceptar os retornos dos métodos do controller e envolvê-los automaticamente, sem a necessidade de modificar a lógica do controller.

1. Anotação de Marcação

Crie uma anotação personalizada para marcar os métodos ou classes que devem ter seus retornos envolvidos:


   import java.lang.annotation.*;

   @Target({ElementType.TYPE, ElementType.METHOD})
   @Retention(RetentionPolicy.RUNTIME)
   @Documented
   public @interface EnvolverRetorno {}
   

2. Interceptor com ResponseBodyAdvice

Implemente a interface ResponseBodyAdvice e anote a classe com @ControllerAdvice. Essa classe interceptará as respostas dos controllers:


   import org.springframework.core.MethodParameter;
   import org.springframework.http.MediaType;
   import org.springframework.http.server.ServerHttpResponse;
   import org.springframework.web.bind.annotation.ControllerAdvice;
   import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

   @ControllerAdvice
   public class ManipuladorRetornoGlobal implements ResponseBodyAdvice<Object> {

       @Override
       public boolean supports(MethodParameter returnType, Class<? extends org.springframework.web.servlet.mvc.method.annotation.MessageConverter> converterType) {
           // Verifica se o método ou classe possui a anotação @EnvolverRetorno
           return returnType.hasMethodAnnotation(EnvolverRetorno.class) ||
                  returnType.getContainingClass().isAnnotationPresent(EnvolverRetorno.class);
       }

       @Override
       public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends org.springframework.web.servlet.mvc.method.annotation.MessageConverter> selectedConverterType, org.springframework.web.server.ServerWebExchange exchange) {
           // Se o retorno for uma exceção, não envolver diretamente
           if (body instanceof Exception) {
               // Tratar exceções de forma padronizada (pode ser outro advice)
               // Por enquanto, vamos retornar uma estrutura genérica de erro
               return ApiResponse.falha(500, "Erro interno no servidor");
           }

           // Envolve o corpo da resposta na estrutura ApiResponse
           return ApiResponse.sucesso(body);
       }
   }
   

3. Controller com Anotação

Agora, basta adicionar a anotação @EnvolverRetorno aos controllers ou métodos que devem ter seus retornos automaticamente formatados:


   @RestController
   @RequestMapping("/api/v1/pedidos")
   @EnvolverRetorno // Aplica a todos os métodos deste controller
   public class PedidoController {

       @Autowired
       private PedidoService servicoPedido;

       @GetMapping("/{id}")
       public Pedido buscarPorId(@PathVariable Long id) {
           // Retorna diretamente o objeto Pedido.
           // O ManipuladorRetornoGlobal irá envolvê-lo.
           return servicoPedido.buscarPorId(id);
       }

       // Exemplo de método sem a anotação (retorno não será envolvido)
       @GetMapping("/status")
       public String obterStatus() {
           return "Serviço ativo";
       }
   }
   

Com essa abordagem, os controllers ficam mais limpos, focados apenas na lógica de negócio, e o retorno da API é consistentemente padronizado, tornando o código mais elegante e fácil de manter.

Tags: java Spring Boot RESTful API AOP design patterns

Publicado em 7-31 20:59