GraphQL promete eliminar over-fetching, under-fetching e a proliferação de endpoints REST específicos por tela. Na prática, também introduz complexidade nova: N+1 queries, caching mais difícil, e uma curva de aprendizado no cliente. Antes de adotar, vale entender quando GraphQL realmente resolve o problema e quando é over-engineering. Este guia usa Hot Chocolate — a biblioteca GraphQL mais completa para .NET — com exemplos práticos.
O problema que GraphQL resolve
Imagine uma tela de dashboard que precisa: dados do usuário logado, seus últimos 5 pedidos (com itens), e notificações não lidas. Em REST, você tem duas opções ruins:
// Opção 1: múltiplos requests (under-fetching)
GET /api/me → dados do usuário
GET /api/pedidos?limit=5 → pedidos (sem itens)
GET /api/pedidos/{id}/itens → para cada pedido (N+1!)
GET /api/notificacoes?lidas=false
// Opção 2: endpoint específico (over-coupling)
GET /api/dashboard → retorna tudo que a tela precisa
// Mas agora o backend está acoplado ao layout da tela específica
Com GraphQL, o cliente declara exatamente o que precisa em uma única query:
query Dashboard {
me {
nome
email
pedidos(limit: 5) {
id
status
itens {
produto
quantidade
preco
}
}
notificacoes(lidas: false) {
id
mensagem
criadaEm
}
}
}
Setup básico com Hot Chocolate
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data.EntityFramework
// Program.cs
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddSubscriptionType<Subscription>()
.AddProjections() // suporte a .Select() automático no EF Core
.AddFiltering() // suporte a filtros em queries
.AddSorting() // suporte a ordenação
.AddInMemorySubscriptions(); // para Subscriptions via WebSocket
app.MapGraphQL(); // /graphql com Banana Cake Pop (IDE integrada)
Queries: buscando dados
[QueryType]
public class Query
{
// Hot Chocolate resolve automaticamente args de paginação, filtros e ordenação
[UsePaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Pedido> GetPedidos(AppDbContext db)
=> db.Pedidos.Include(p => p.Itens);
public async Task<Pedido?> GetPedidoAsync(
int id,
PedidosByIdDataLoader loader,
CancellationToken ct)
=> await loader.LoadAsync(id, ct); // DataLoader para evitar N+1
[Authorize]
public async Task<Usuario?> GetMeAsync(
[Service] IHttpContextAccessor ctx,
UsersByIdDataLoader loader,
CancellationToken ct)
{
var userId = ctx.HttpContext?.User.GetUserId();
return userId.HasValue ? await loader.LoadAsync(userId.Value, ct) : null;
}
}
O problema N+1 e DataLoaders
O maior risco de GraphQL é o N+1: se cada pedido carrega seus itens de forma independente, uma query de 20 pedidos gera 21 queries no banco. DataLoaders resolvem isso com batching automático:
// DataLoader agrupa todas as requisições de uma execução em um único batch
public class ItensByPedidoIdDataLoader : GroupedDataLoader<int, Item>
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
public ItensByPedidoIdDataLoader(
IDbContextFactory<AppDbContext> dbFactory,
IBatchScheduler scheduler,
DataLoaderOptions options) : base(scheduler, options)
{
_dbFactory = dbFactory;
}
// Chamado UMA vez com todos os IDs coletados na execução
protected override async Task<ILookup<int, Item>> LoadGroupedBatchAsync(
IReadOnlyList<int> pedidoIds,
CancellationToken ct)
{
await using var db = await _dbFactory.CreateDbContextAsync(ct);
var itens = await db.Itens
.Where(i => pedidoIds.Contains(i.PedidoId))
.ToListAsync(ct);
// Hot Chocolate agrupa pelo ID do pedido automaticamente
return itens.ToLookup(i => i.PedidoId);
}
}
// Type extension: onde o DataLoader é usado
[ExtendObjectType(typeof(Pedido))]
public class PedidoExtensions
{
public async Task<IEnumerable<Item>> GetItensAsync(
[Parent] Pedido pedido,
ItensByPedidoIdDataLoader loader,
CancellationToken ct)
=> await loader.LoadAsync(pedido.Id, ct);
}
Com DataLoader, 20 pedidos = 2 queries no banco (uma para pedidos, uma para todos os itens dos 20 pedidos em batch), não 21.
Mutations: escrevendo dados
[MutationType]
public class Mutation
{
public async Task<MutationResult<Pedido, UserError[]>> CriarPedidoAsync(
CriarPedidoInput input,
[Service] IPedidoService service,
CancellationToken ct)
{
var validacao = ValidarInput(input);
if (!validacao.IsValid)
return new UserError[] {
new(validacao.ErrorMessage, "INVALID_INPUT")
};
var pedido = await service.CriarAsync(input, ct);
return pedido;
}
}
// Input type — Hot Chocolate gera automaticamente a documentação
public record CriarPedidoInput(
[property: ID(nameof(Cliente))] int ClienteId,
List<PedidoItemInput> Itens
);
public record PedidoItemInput(
[property: ID(nameof(Produto))] int ProdutoId,
int Quantidade
);
Subscriptions: dados em tempo real via WebSocket
[SubscriptionType]
public class Subscription
{
[Subscribe]
[Topic("{clienteId}")]
public Pedido OnPedidoAtualizado(
[EventMessage] Pedido pedido) => pedido;
}
// Publicar evento (em qualquer parte do sistema)
await eventSender.SendAsync(
topicName: $"{clienteId}",
message: pedidoAtualizado,
cancellationToken: ct);
// Cliente GraphQL (JavaScript)
const { data } = useSubscription(gql`
subscription OnPedidoAtualizado($clienteId: ID!) {
onPedidoAtualizado(clienteId: $clienteId) {
id
status
atualizadoEm
}
}
`, { variables: { clienteId } });
Quando GraphQL vale o investimento
Use GraphQL quando: você tem múltiplos clientes com necessidades diferentes do mesmo dado (app mobile, web, parceiros API), o frontend evolui rapidamente e precisaria de novos endpoints REST a cada mudança de tela, o produto tem um grafo de dados rico com muitas relações (e-commerce com produto/categoria/review/vendedor/estoque), ou você quer expor uma API pública onde consumidores externos declaram suas próprias queries.
Não use GraphQL quando: é um CRUD simples com 5-10 endpoints — REST é mais simples, performático para caching HTTP, e familiar. Quando sua API é consumida apenas por um cliente que você controla, e REST já atende sem proliferação de endpoints. Quando o time não tem experiência com DataLoaders e caching de GraphQL — o risco de N+1 em produção é real. Quando você precisa de download de arquivo, upload, ou streaming — REST handles esses casos mais naturalmente.
Caching: o ponto fraco do GraphQL
REST tem caching HTTP nativo por URL. GraphQL usa POST com body variável — não há cache HTTP automático. As alternativas:
// 1. Persisted Queries: hash da query como ID único (cacheable em CDN)
// Cliente envia apenas o hash, servidor mapeia para a query completa
builder.Services
.AddGraphQLServer()
.UsePersistedQueryPipeline()
.AddReadOnlyFileSystemQueryStorage("./persisted-queries");
// 2. Cache na camada de resolução (para dados frequentes)
public class Query
{
[UseFirstOrDefault]
[UseProjection]
public IQueryable<Produto> GetProdutoDestaque(
[Service] IMemoryCache cache,
AppDbContext db)
{
if (cache.TryGetValue("produto-destaque", out Produto? cached))
return new[] { cached! }.AsQueryable();
// ... busca do banco com cache por 5 minutos
}
}
GraphQL no .NET com Hot Chocolate é uma escolha madura e pronta para produção. O framework resolve os problemas mais complexos (N+1, paginação, autorização por campo, subscriptions) com anotações declarativas. O investimento vale quando o problema que você está resolvendo — múltiplos clientes com queries variadas sobre o mesmo grafo de dados — realmente existe. Para CRUDs simples ou APIs com contratos estáveis, REST continua sendo a escolha certa por sua simplicidade operacional e caching nativo.