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.