Pular para o conteúdo

APIs - REST, GraphQL e gRPC

Todos os três resolvem “como um processo pede algo a outro”, mas otimizam variáveis diferentes: acoplamento de contrato, flexibilidade da consulta, desempenho de serialização e maturidade do ecossistema. Escolher é priorizar entre elas.

REST expõe recursos identificados por URIs e manipulados pela semântica uniforme do HTTP (verbos, status codes, cabeçalhos, cache). Sua força é a ubiquidade: qualquer cliente HTTP consome, cache de infraestrutura funciona de graça, e a curva de adoção é mínima.

  • Nível 0: HTTP como túnel RPC (um endpoint, tudo POST).
  • Nível 1: recursos distintos por URI.
  • Nível 2: uso correto de verbos (GET/POST/PUT/DELETE) e status codes.
  • Nível 3: HATEOAS — respostas carregam links para as próximas transições de estado.

A maioria das APIs “REST” fica no nível 2, o que é pragmático e suficiente. O ponto fraco do REST é o over/under-fetching: o recurso tem forma fixa, então o cliente móvel recebe campos que não usa (over) ou precisa de várias chamadas para compor uma tela (under).

GraphQL inverte o controle: o servidor publica um schema tipado e o cliente declara exatamente quais campos quer numa única query. Elimina over/under-fetching e é ideal para clientes heterogêneos (web, mobile) com necessidades de dados divergentes evoluindo rápido.

Custos:

  • Cache é mais difícil (POST único, resposta variável) do que o cache de recurso do REST.
  • Problema N+1: um resolver por campo de lista pode disparar uma consulta por item. Mitiga-se com DataLoader (batching + cache por request).
  • Complexidade de query: clientes podem montar consultas caras; exige limites de profundidade/custo.

gRPC é RPC com contrato Protobuf compilado, trafegando binário sobre HTTP/2. Vantagens: serialização compacta e rápida, contrato fortemente tipado gerando stubs em várias linguagens, e streaming bidirecional nativo. É a escolha natural para comunicação interna de alta frequência entre serviços.

Custos: menos amigável a navegadores (exige gRPC-Web + proxy), payload binário não é human-readable, e o acoplamento ao contrato compilado é mais rígido — o que é uma vantagem para contratos internos e uma fricção para APIs públicas.

Critério REST GraphQL gRPC
Público-alvo Clientes externos, APIs públicas Clientes ricos/heterogêneos (mobile, web) Serviço-a-serviço interno
Contrato OpenAPI (opcional) Schema GraphQL (forte) Protobuf (forte, compilado)
Transporte HTTP/1.1+ HTTP (POST) HTTP/2
Serialização JSON (texto) JSON (texto) Protobuf (binário)
Over/under-fetching Sofre Resolve Contrato fixo por método
Streaming Limitado (SSE) Subscriptions Nativo bidirecional
Cache HTTP Excelente Fraco Não aplicável
Melhor cenário CRUD público, cacheável Agregação flexível para UI Baixa latência interna
graph LR
    Mobile[Cliente Mobile] -->|GraphQL| GW[Gateway BFF]
    Web[Cliente Web] -->|REST| GW
    GW -->|gRPC| S1[Servico Pedido]
    GW -->|gRPC| S2[Servico Estoque]
    S1 -->|gRPC stream| S2
@RestController
@RequestMapping("/pedidos")
public class PedidoController {
private final PedidoAppService service;
public PedidoController(PedidoAppService service) {
this.service = service;
}
@PostMapping
public ResponseEntity<PedidoResponse> criar(@RequestBody @Valid CriarPedidoRequest req) {
UUID id = service.criar(req.clienteId(), req.total());
return ResponseEntity
.created(URI.create("/pedidos/" + id))
.body(new PedidoResponse(id, "CRIADO"));
}
@GetMapping("/{id}")
public PedidoResponse buscar(@PathVariable UUID id) {
return service.buscar(id);
}
}
record CriarPedidoRequest(@NotNull UUID clienteId, @Positive BigDecimal total) {
}
record PedidoResponse(UUID id, String status, BigDecimal total) {
}
type Query {
pedido(id: ID!): Pedido
pedidosDoCliente(clienteId: ID!): [Pedido!]!
}
type Pedido {
id: ID!
status: String!
total: Float!
itens: [Item!]!
}
type Item {
sku: String!
quantidade: Int!
}
@Controller
public class PedidoGraphqlController {
private final PedidoAppService service;
private final ItemLoader itemLoader;
public PedidoGraphqlController(PedidoAppService service, ItemLoader itemLoader) {
this.service = service;
this.itemLoader = itemLoader;
}
@QueryMapping
public PedidoResponse pedido(@Argument UUID id) {
return service.buscar(id);
}
@SchemaMapping(typeName = "Pedido", field = "itens")
public CompletableFuture<List<Item>> itens(PedidoResponse pedido) {
return itemLoader.load(pedido.id());
}
}

O @SchemaMapping do campo itens resolvido por um loader com batching é a defesa contra o problema N+1: em vez de uma query por pedido, os IDs são agrupados por request.

syntax = "proto3";
package pedido;
option java_multiple_files = true;
option java_package = "com.exemplo.pedido.grpc";
service PedidoService {
rpc Buscar(BuscarRequest) returns (PedidoReply);
rpc StreamStatus(BuscarRequest) returns (stream StatusEvento);
}
message BuscarRequest {
string id = 1;
}
message PedidoReply {
string id = 1;
string status = 2;
double total = 3;
}
message StatusEvento {
string status = 1;
int64 timestamp = 2;
}
  • REST nível 0: um único endpoint POST fazendo RPC disfarçado — perde cache, verbos e status codes.
  • GraphQL sem DataLoader: resolvers ingênuos geram N+1 e derrubam o banco.
  • GraphQL sem limite de custo: clientes montam queries profundas e caras (DoS acidental).
  • gRPC exposto direto ao navegador sem gRPC-Web/proxy, ou usado onde cache HTTP seria essencial.
  • Verbos e status errados no REST: 200 OK para erro, GET que muda estado — quebrando a semântica safe do método (GET deve ser seguro e idempotente) e, de quebra, o cache.
  1. O que é over/under-fetching no REST e como GraphQL o resolve?
Resposta

Recursos REST têm forma fixa: o cliente recebe campos que não usa (over-fetching) ou precisa de várias chamadas para compor uma tela (under-fetching). GraphQL deixa o cliente declarar exatamente os campos desejados numa única query tipada pelo schema, eliminando ambos.

  1. Por que gRPC é preferido para tráfego interno e menos indicado para clientes externos?
Resposta

Interno: Protobuf binário sobre HTTP/2 dá baixa latência, contrato tipado e streaming, ideal para chamadas frequentes serviço-a-serviço. Externo: payload não legível, suporte limitado em navegador (exige gRPC-Web + proxy) e ausência de cache HTTP tornam REST/GraphQL melhores.

  1. O que é o problema N+1 em GraphQL e como mitigá-lo?
Resposta

Um resolver por item de uma lista dispara uma consulta por elemento (1 da lista + N dos itens). Mitiga-se com um DataLoader que agrupa (batching) os IDs e cacheia por request, transformando N+1 consultas em poucas.

  1. O que separa os níveis 2 e 3 do Modelo de Maturidade de Richardson?
Resposta

O nível 2 usa recursos, verbos HTTP e status codes corretamente. O nível 3 adiciona HATEOAS: as respostas incluem links (hypermedia) para as próximas transições de estado, tornando o cliente menos acoplado às URIs.