.NET GraphQL Hot Chocolate API Backend Arquitetura

GraphQL no .NET com Hot Chocolate: quando vale substituir o REST

Guia prático de GraphQL com Hot Chocolate no .NET: quando vale sair do REST, queries, mutations, subscriptions, N+1 com DataLoader e integração com EF Core.

N
Neryx Digital Architects
17 de março de 2026
13 min de leitura
240 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.