A pergunta errada e a pergunta certa
Toda semana alguém pergunta: "REST ou GraphQL?" Essa é a pergunta errada. A pergunta certa é: "Quem vai consumir essa API, com que frequência, com que padrão de acesso, e quais são os custos de manutenção que o time consegue absorver?"
REST, GraphQL e gRPC resolvem problemas diferentes. Cada um tem um custo de implementação, um custo de operação e um conjunto de problemas que resolve bem — e problemas que complica. Este guia é sobre entender esses trade-offs, não sobre escolher o mais "moderno".
REST: o padrão que sobrevive há 25 anos por uma razão
REST (Representational State Transfer) é o default por uma razão simples: é o modelo mais amplamente suportado, mais fácil de depurar e mais fácil de entender para qualquer desenvolvedor.
O que REST faz bem
Recursos bem definidos com URLs semânticas. Verbos HTTP com semântica clara (GET, POST, PUT, PATCH, DELETE). Cache nativo via HTTP headers. Tooling excelente (Swagger/OpenAPI, Postman, navegador como client). Stateless por design. Todo desenvolvedor conhece.
// REST endpoint bem projetado no .NET
app.MapGet("/clientes/{id:guid}/pedidos", async (
Guid id,
[FromQuery] int pagina = 1,
[FromQuery] int tamanho = 20,
[FromQuery] string? status,
ISender sender,
CancellationToken ct) =>
{
var query = new GetPedidosClienteQuery(id, pagina, tamanho, status);
var resultado = await sender.Send(query, ct);
return resultado.IsEmpty
? Results.NotFound()
: Results.Ok(resultado);
})
.WithName("GetPedidosCliente")
.WithOpenApi()
.Produces<PagedResult<PedidoSummaryDto>>()
.Produces(404);
Onde REST tem fricção
Over-fetching: você precisa de nome e foto do cliente, mas o endpoint retorna 30 campos. Solução: campos de projeção (?fields=nome,foto) — funciona mas é não-padrão.
Under-fetching: para montar a tela do painel do usuário você precisa de 5 endpoints diferentes. N+1 de endpoints. Solução: endpoints de composição (/dashboard) — cria acoplamento frontend-backend.
Versionamento: /v1/pedidos, /v2/pedidos — versão na URL é simples mas gera código duplicado. Header versioning (Accept: application/vnd.api+json;version=2) é mais limpo mas menos visível.
Quando REST é a escolha certa
- APIs públicas ou para parceiros externos — todo mundo sabe consumir REST
- APIs simples com recursos bem definidos e relacionamentos claros
- Quando caching de HTTP é importante (GET de recursos públicos)
- Times sem experiência prévia com GraphQL ou gRPC
- Integrações com sistemas legados ou externos que esperam REST
GraphQL: poder para o frontend, custo para o backend
GraphQL foi criado pelo Facebook para resolver um problema específico: um grafo de dados complexo com múltiplos tipos de clientes (mobile, web, parceiros) com necessidades de dados completamente diferentes.
O que GraphQL faz bem
O cliente declara exatamente os dados que precisa — nem mais, nem menos. Uma única requisição pode buscar dados de múltiplos recursos. Tipagem forte com schema auto-documentado. Evolução de API sem versioning (adicionar campos é non-breaking).
# Cliente pede exatamente o que precisa
query PainelCliente($clienteId: ID!) {
cliente(id: $clienteId) {
nome
foto
pedidos(ultimosN: 5) {
id
status
valorTotal
dataConfirmacao
itens {
produto { nome }
quantidade
}
}
creditoDisponivel
}
}
Uma requisição. Dados exatos. Sem over-fetching, sem under-fetching.
Setup no .NET com Hot Chocolate
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddProjections()
.AddFiltering()
.AddSorting()
.AddAuthorization();
// Query type
public class Query
{
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Cliente> GetClientes([Service] AppDbContext db) =>
db.Clientes.AsNoTracking();
public async Task<Cliente?> GetCliente(
Guid id,
[Service] IClienteRepository repo,
CancellationToken ct) =>
await repo.GetByIdAsync(id, ct);
}
O que os tutoriais escondem sobre GraphQL
N+1 problem: uma query que pede 100 clientes e seus pedidos pode disparar 101 queries no banco. Requer DataLoader para resolver:
public class PedidosByClienteDataLoader : BatchDataLoader<Guid, List<Pedido>>
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
protected override async Task<IReadOnlyDictionary<Guid, List<Pedido>>>
LoadBatchAsync(IReadOnlyList<Guid> clienteIds, CancellationToken ct)
{
await using var db = await _dbFactory.CreateDbContextAsync(ct);
return await db.Pedidos
.Where(p => clienteIds.Contains(p.ClienteId))
.GroupBy(p => p.ClienteId)
.ToDictionaryAsync(g => g.Key, g => g.ToList(), ct);
}
}
Queries maliciosas: um cliente pode pedir dados circulares ou queries extremamente profundas, sobrecarregando o servidor. Precisa de depth limiting e complexity analysis:
builder.Services
.AddGraphQLServer()
.AddMaxExecutionDepthRule(maxAllowedExecutionDepth: 5)
.AddOperationComplexityAnalyzer(o => o.MaximumAllowed = 1000);
Caching HTTP não funciona: toda query GraphQL é um POST. Sem cache nativo de HTTP. Requer caching na camada de aplicação ou Apollo Client no frontend.
Curva de aprendizado real: DataLoader, resolvers, fragments, subscriptions, federation — o ecosistema é poderoso mas tem profundidade.
Quando GraphQL faz sentido
- API consumida por múltiplos clientes com necessidades de dados muito diferentes (app mobile, web, TV)
- Grafo de dados complexo com muitos relacionamentos
- Frontend tem autonomia para evoluir sem mudanças de backend
- Time de frontend maduro que vai usar as capacidades avançadas
- Sistema de BFF (Backend for Frontend) que agrega múltiplos serviços
gRPC: performance máxima para comunicação interna
gRPC usa Protobuf (Protocol Buffers) para serialização binária e HTTP/2 para transporte. É tipicamente 5-10x mais rápido que REST em throughput, com payloads menores.
O que gRPC faz bem
Serialização binária eficiente. Streaming bidirecional nativo (HTTP/2). Contratos tipados via .proto (geração de código em qualquer linguagem). Perfeito para comunicação entre microsserviços onde performance importa.
// pedidos.proto
syntax = "proto3";
service PedidoService {
rpc GetPedido (GetPedidoRequest) returns (PedidoResponse);
rpc ListarPedidos (ListarPedidosRequest) returns (stream PedidoResponse);
rpc ProcessarPedido (ProcessarRequest) returns (ProcessarResponse);
}
message GetPedidoRequest {
string pedido_id = 1;
}
message PedidoResponse {
string id = 1;
string status = 2;
double valor_total = 3;
int64 created_at = 4;
}
// Implementação no .NET
public class PedidoGrpcService : PedidoService.PedidoServiceBase
{
private readonly ISender _sender;
public override async Task<PedidoResponse> GetPedido(
GetPedidoRequest request,
ServerCallContext context)
{
var pedido = await _sender.Send(
new GetPedidoQuery(Guid.Parse(request.PedidoId)),
context.CancellationToken);
return new PedidoResponse
{
Id = pedido.Id.ToString(),
Status = pedido.Status.ToString(),
ValorTotal = (double)pedido.ValorTotal.Amount
};
}
public override async Task ListarPedidos(
ListarPedidosRequest request,
IServerStreamWriter<PedidoResponse> responseStream,
ServerCallContext context)
{
await foreach (var pedido in _sender.CreateStream<PedidoResponse>(
new StreamPedidosQuery(request.ClienteId), context.CancellationToken))
{
await responseStream.WriteAsync(pedido);
}
}
}
O que gRPC não resolve
Browser support é limitado (precisa de gRPC-Web). Debugging é difícil (binary protocol — não dá pra ler no Wireshark sem ferramentas). Load balancers tradicionais precisam de suporte a HTTP/2. Protobuf schema evolution tem regras rígidas (não delete campos, só marque deprecated).
Quando gRPC faz sentido
- Comunicação interna entre microsserviços (não exposta ao browser)
- Alta frequência de chamadas onde latência e throughput importam
- Streaming de dados (logs em tempo real, eventos, pipelines de dados)
- Múltiplas linguagens que precisam se comunicar (geração de client code a partir do .proto)
- Substituição de comunicação síncrona em hot paths
Comparativo direto
| Critério | REST | GraphQL | gRPC |
|--------------------|---------------|----------------|----------------|
| Facilidade inicial | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Performance | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Flexibilidade API | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Suporte browser | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| Caching HTTP | ⭐⭐⭐⭐⭐ | ⭐ | ⭐ |
| Streaming | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Tooling/Ecosystem | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Curva aprendizado | Baixa | Média-Alta | Média |
A arquitetura híbrida que funciona na prática
A maioria dos sistemas modernos usa os três:
[ Browser / App Mobile ]
|
[ REST ou GraphQL ] ← API Gateway / BFF público
|
[ gRPC ] ← Comunicação interna entre microsserviços
|
[ Mensageria (eventos) ] ← Comunicação assíncrona entre contextos
REST ou GraphQL para APIs externas (clientes, parceiros). gRPC para comunicação interna em hot paths. Mensageria para comunicação assíncrona e eventos de domínio.
Conclusão
Use REST como default para APIs externas — é simples, universal e bem compreendido. Migre para GraphQL quando o frontend precisar de flexibilidade real de dados e o time estiver preparado para lidar com DataLoaders e complexidade. Adote gRPC nos hot paths internos onde performance é medida e justifica o custo de operação.
A pior decisão é escolher GraphQL "porque é moderno" sem entender o N+1 problem. A segunda pior é rejeitar gRPC por ser "complexo" sem medir a diferença de latência nos serviços que mais precisam de performance.