Introdução
GraphQL é uma linguagem de consulta para APIs e um runtime que executa essas consultas sobre um schema tipado. Em vez de receber sempre uma representação fixa de um recurso, o cliente descreve os campos de que precisa; o servidor valida a operação, chama os resolvers responsáveis e devolve um JSON com aquele formato. Eu consideraria GraphQL quando diferentes clientes precisam de combinações variadas de dados ou quando uma tela depende de informações relacionadas que hoje exigem várias chamadas, desde que a equipe também esteja preparada para controlar o custo das consultas.
Essa flexibilidade não torna GraphQL automaticamente mais rápido, menor ou mais simples que REST. Ela desloca parte do trabalho para o schema e para os resolvers: o cliente escolhe os campos, mas o servidor continua responsável por autorização, validação, limites de consulta, paginação e acesso eficiente aos dados. Uma query mal controlada ainda pode fazer o backend executar trabalho excessivo.
Neste guia eu explico schema, types, queries, mutations e resolvers, mostro uma implementação executável com GraphQL.NET no ASP.NET Core/.NET 10 e comparo a mesma leitura com um endpoint REST. Também vou separar o que a demo realmente implementa das medidas que uma API de produção precisa acrescentar. Este é o segundo artigo da série APIs e Comunicação em .NET: a parte 1 apresenta gRPC e seus critérios de uso; a próxima será um guia específico de SignalR.
ℹ️ Informação: O sample testado usa GraphQL.NET 8.8.5, GraphQL.Server.Transports.AspNetCore 8.3.3 e .NET SDK 10.0.402. A query, a mutation, a validação e o endpoint REST foram exercitados por HTTP no processo real da API.
Pré-requisitos
Para acompanhar o exemplo, você precisa de C# básico, familiaridade com HTTP e .NET SDK 10.0.400 ou superior. O código completo está no repositório BlogSamples; clone-o e inicie a API com a URL explicitada:
git clone https://github.com/lzocateli/blog-zocateli-sample.git
cd blog-zocateli-sample
dotnet run --project src/BlogSamples --urls http://localhost:5101
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
A API agregadora também inicia outros domínios do repositório. O worker de outbox existente depende de PostgreSQL e registra falhas de conexão se o serviço não estiver disponível; isso não é dependência do tutorial GraphQL. Os endpoints deste exemplo usam o serviço de produtos em memória, e qualquer produto criado desaparece quando o processo termina.
O que GraphQL é e como uma operação é executada
GraphQL não é sinônimo de JSON nem de HTTP. GraphQL define uma linguagem para expressar operações e uma especificação para schemas, tipos, validação e execução. HTTP costuma transportar a operação e a resposta, mas o transporte é uma camada separada. Uma API GraphQL pode usar um endpoint HTTP como /graphql; a query é enviada no corpo de uma requisição POST ou, em cenários apropriados, em uma requisição GET.
O schema (esquema) é o contrato que descreve os tipos, campos, argumentos e operações que o servidor aceita. Uma query percorre esse grafo de campos. Por exemplo, product pode retornar um Product, e esse tipo pode oferecer id, name, price e category. O contrato descreve o que pode ser solicitado, não necessariamente como os dados estão guardados: eles podem vir de uma tabela, de uma API externa, de cache ou de mais de uma fonte.
Em uma requisição, o servidor primeiro interpreta a sintaxe da operação. Depois valida se os campos e argumentos existem no schema e se os tipos são compatíveis. Se a operação passar, o executor resolve os campos solicitados e monta uma resposta JSON com a mesma forma da seleção. Uma falha de validação impede a operação antes de chamar os resolvers; uma falha durante a execução pode retornar data parcial ou nulo junto com errors.
⚠️ Atenção: A linguagem GraphQL define a operação, não uma promessa de transporte universal. O exemplo usa HTTP POST com JSON; WebSockets e subscriptions são outro recorte, não fazem parte desta implementação.
Schema, types, queries, mutations e inputs
Um type é um tipo do schema. O GraphQL define scalars como String, Int, Float, Boolean e ID; o schema também pode declarar enums, tipos de objeto e input objects. Product e Category são tipos de saída: descrevem os dados que o servidor pode devolver. Um input object agrupa valores recebidos, geralmente para uma mutation com vários parâmetros.
O schema tem um root type de leitura, normalmente chamado Query, e pode ter um root Mutation para operações de escrita. Subscription é usado para atualizações contínuas quando a implementação escolhe um transporte compatível. Esses nomes são convenções do contrato, não nomes de tabelas ou classes obrigatórios. Um campo pode receber argumentos e retornar um objeto, uma lista, um scalar ou um valor nullable.
O exemplo usa CreateProductInput para agrupar name, description, price, stock e categoryId. Isso deixa a assinatura da mutation legível e facilita evoluir os dados de entrada. No resolver, os valores são convertidos para o CriarProdutoRequest já usado pela API. A entrada passa por validação explícita de nome, preço, estoque e categoria antes da chamada ao serviço.
Um enum limita um campo a valores nomeados, como DRAFT e PUBLISHED, em vez de aceitar qualquer string. Tipos não-null (Product!) e listas ([Product!]!) também expressam nulabilidade no contrato; use essas garantias somente quando a aplicação consegue cumpri-las.
Uma mutation não substitui regras de domínio nem uma transação. O schema determina a forma de entrada e saída; o resolver chama a aplicação, que deve validar as invariantes, autorizar o usuário e controlar persistência e efeitos colaterais. No sample, o armazenamento é deliberadamente em memória e não existe autenticação. Isso é suficiente para demonstrar o fluxo, não para representar uma API de produção.
O papel dos resolvers e o problema N+1
Um resolver é a função que obtém o valor de um campo. Ele pode ler uma propriedade já carregada, consultar um serviço ou compor resultados de fontes diferentes. Argumentos da query chegam ao resolver pelo contexto de execução; dependências de aplicação podem ser acessadas pelo container de injeção de dependência. O schema não exige que cada campo faça uma consulta própria: campos irmãos podem compartilhar dados, e uma camada de aplicação pode devolver um objeto já preparado.
No sample, o resolver do root product(id:) chama IProdutoService.ObterProdutoPorIdAsync. O campo category usa o CategoriaId do DTO e pede a categoria ao mesmo serviço. Em uma query para um único produto, isso é fácil de acompanhar. Se o schema passasse a listar dezenas de produtos e cada item buscasse sua categoria individualmente, o padrão poderia gerar uma consulta para a lista e mais uma para cada categoria: o problema N+1.
O DataLoader agrupa chaves solicitadas durante uma execução e permite buscar vários valores em lote; ele também pode reaproveitar o resultado para chaves repetidas dentro daquele contexto. Com 50 pedidos que carregam usuários, por exemplo, a aplicação pode fazer uma busca dos pedidos e uma busca em lote dos usuários, em vez de 51 acessos separados. O ganho depende de a fonte oferecer uma operação em lote e de o loader estar configurado no escopo correto; não é uma otimização automática de qualquer resolver.
Por isso, mantenha resolvers finos: encaminhe a operação a serviços que concentrem autorização e regras de domínio, limite listas e propague cancelamento e erros. A demo não implementa DataLoader porque consulta apenas um produto e uma categoria, sem uma lista que exponha N+1.
GraphQL.NET no ASP.NET Core e .NET 10
GraphQL.NET fornece o executor, os tipos e os recursos de integração com injeção de dependência. O transporte ASP.NET Core adiciona middleware HTTP para receber operações no endpoint. No sample, GraphQL e seus pacotes de DI e serialização estão fixados em 8.8.5, enquanto GraphQL.Server.Transports.AspNetCore está em 8.3.3; os pacotes verificados declaram licença MIT e targets compatíveis com frameworks superiores.
A documentação atual de integração GraphQL.NET apresenta um quickstart cujo TargetFramework é net10.0. A página de NuGet do transporte 8.3.3 ainda descreve compatibilidade do middleware até ASP.NET Core 9. Como essas informações não estão alinhadas, eu não trato a declaração de targets do NuGet como prova isolada do runtime: restaurei e compilei os pacotes no SDK 10.0.402 e executei query, mutation e erro de validação no endpoint real do sample em .NET 10.
O registro de serviços liga o schema à implementação JSON. Os graph types que o schema descobre também precisam estar disponíveis no DI:
// Registra os graph types e o schema tipado da API.
builder.Services.AddSingleton<ProdutoGraphType>();
builder.Services.AddSingleton<CategoriaGraphType>();
builder.Services.AddSingleton<CriarProdutoInputGraphType>();
builder.Services.AddGraphQL(options => options
.AddSchema<ProdutoSchema>()
.AddSystemTextJson());
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
Depois de construir a aplicação, o middleware recebe operações em /graphql:
app.UseWebSockets();
app.UseGraphQL<ProdutoSchema>("/graphql");
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
O sample registra ProdutoSchema, cujo root Query contém product(id:) e cujo root Mutation contém createProduct(input:). Não há GraphiQL, subscriptions, banco de dados novo ou outra engine GraphQL neste exemplo.
Exemplo Prático
Vou consultar o produto de ID 1, que no seed da API é o Smartphone Galaxy S25. A query pede somente identificador, nome, preço e nome da categoria; as variáveis ficam separadas do texto da operação, o que facilita reutilizar a query sem interpolar valores na string.
# Envia a operação e o argumento id como variável JSON.
curl -i http://localhost:5101/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"query($id:Int!){product(id:$id){id name price category{name}}}","variables":{"id":1}}'
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
Resposta observada no sample:
{"data":{"product":{"id":1,"name":"Smartphone Galaxy S25","price":4299.00,"category":{"name":"Eletrônicos"}}}}
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
Agora faço a leitura REST do mesmo ID:
curl -i http://localhost:5101/api/products/1
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
{"id":1,"name":"Smartphone Galaxy S25","description":"Tela AMOLED 6.2\", 256GB, 5G","price":4299.00,"stock":45,"active":true,"categoryId":1,"categoryName":"Eletrônicos"}
O endpoint REST do sample retorna a representação que seu contrato definiu; a query GraphQL do exemplo pediu uma projeção específica no próprio documento. Isso ilustra duas interfaces concretas, não uma limitação intrínseca do REST. Uma API REST também pode ter endpoints especializados, parâmetros de projeção ou representações diferentes. Os dois JSONs são da mesma fonte em memória, mas cada contrato expõe um formato distinto.
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
Para criar um produto, envio um CreateProductInput. O primeiro produto criado em uma execução limpa recebeu o ID 53; a mutation retorna esse valor e os campos selecionados:
curl -i http://localhost:5101/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"mutation($input:CreateProductInput!){createProduct(input:$input){id name price category{name}}}","variables":{"input":{"name":"Mouse GraphQL","description":"Produto de demonstração","price":99.90,"stock":10,"categoryId":1}}}'
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
{"data":{"createProduct":{"id":53,"name":"Mouse GraphQL","price":99.90,"category":{"name":"Eletrônicos"}}}}
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
O resolver rejeita um preço igual a zero antes de chamar IProdutoService. O transporte retorna HTTP 200, mas a resposta inclui errors e createProduct: null; por isso, um cliente GraphQL deve examinar o corpo da resposta, não apenas o status HTTP.
curl -i http://localhost:5101/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"mutation($input:CreateProductInput!){createProduct(input:$input){id name price}}","variables":{"input":{"name":"Produto inválido","price":0,"stock":10,"categoryId":1}}}'
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
{"errors":[{"message":"Preço deve ser entre R$ 0,01 e R$ 999.999,99.","path":["createProduct"]}],"data":{"createProduct":null}}
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
Uma query que tenta selecionar um campo inexistente é rejeitada antes da execução. No servidor testado, essa falha de validação retornou HTTP 400 com o código FIELDS_ON_CORRECT_TYPE.
curl -i http://localhost:5101/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ product(id:1) { missingField } }"}'
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
{"errors":[{"message":"Cannot query field 'missingField' on type 'Product'.","extensions":{"code":"FIELDS_ON_CORRECT_TYPE","number":"5.3.1"}}]}
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/GraphQL/
Segurança e limites operacionais
GraphQL não substitui autenticação nem autorização. Saber o ID de um objeto não prova que o usuário pode lê-lo ou alterá-lo. O servidor deve verificar autorização no nível do recurso e, quando necessário, de cada campo; mutations precisam validar o usuário e a regra de domínio antes de produzir efeitos. Uma checagem apenas no endpoint pode ser insuficiente quando o mesmo schema expõe dados com níveis de acesso diferentes.
A flexibilidade também cria um orçamento de execução. Uma API que aceita listas deve impor paginação com limite máximo, restringir profundidade e complexidade quando o grafo permite consultas caras, controlar batching e aplicar timeouts e rate limits por identidade ou cliente. O tamanho do corpo HTTP também precisa de limite. Contar requests isoladamente não mede o trabalho: uma única operação pode selecionar muitos campos ou repetir chamadas a objetos diferentes.
Introspection permite que clientes consultem o próprio schema. Ela é útil para ferramentas, mas avalie sua exposição; restringi-la não substitui autorização ou limites de custo. Não exponha GraphiQL sem necessidade e evite stack traces e detalhes internos em erros de produção.
O transporte GraphQL.NET 8.3.3 habilita proteção CSRF por padrão e não habilita processamento de formulários POST por padrão. Ainda assim, uma API com autenticação por cookie precisa revisar métodos aceitos, CORS, headers e mecanismos de CSRF na configuração real. Para mutations, use POST; não trate validação de tipos como autorização, e não confie em dados do cliente sem validar invariantes no domínio.
Na demo, a validação de nome, preço, estoque e categoria está implementada, mas a API não tem login, política de autorização, rate limit, limite de complexidade ou persistência. A introspection permanece disponível e não existe interface GraphiQL. Esses são limites deliberados do exemplo didático; não os interprete como configuração recomendada para uma API pública.
⚠️ Atenção: O schema descreve campos disponíveis, não quem pode acessá-los. Cada resolver ou caso de uso precisa aplicar as regras de acesso ao dado e à operação.
GraphQL, REST e gRPC: como escolher
Eu começaria pelos consumidores, contrato e tráfego, não pela preferência da equipe. GraphQL atende combinações variáveis de dados; REST organiza recursos e semântica HTTP; gRPC define métodos e mensagens tipadas para serviços controlados e streaming.
| Critério | REST | GraphQL | gRPC |
|---|---|---|---|
| Contrato | Recursos, URLs e semântica HTTP | Schema tipado e campos selecionáveis | Serviços e mensagens em contrato Protobuf |
| Seleção de dados | Representação definida pelo endpoint; pode ter projeções | Seleção escrita em cada query | Estrutura da mensagem definida pelo método |
| Cache | Integra-se diretamente às semânticas de cache HTTP para GET | Depende do transporte, da operação e da estratégia de cache adotada | Normalmente exige estratégia da aplicação ou intermediário compatível |
| Streaming | HTTP permite streaming, conforme endpoint e formato | Subscriptions dependem de um transporte e protocolo configurados | Streaming unary, servidor, cliente ou bidirecional faz parte do modelo RPC |
| Cliente comum | Navegadores, serviços e ferramentas HTTP | Clientes que entendem o schema e enviam operações GraphQL | Serviços com contrato e cliente gerado; browser pode precisar de gRPC-Web |
| Custo operacional | Modelar recursos e evoluir representações | Operar schema, resolvers, autorização e limites de query | Gerar/versionar contratos, configurar HTTP/2 e operar streams |
No exemplo acima, GET /api/products/1 devolve descrição, estoque e categoria achatada, enquanto a query GraphQL seleciona id, name, price e category { name }. Isso mostra o contrato que implementamos: não prova que REST sempre devolva campos demais. Uma rota REST poderia oferecer projeção ou uma representação menor; compare contratos concretos em vez de padrões caricaturados.
Para cliente web com leituras compostas e requisitos variáveis, GraphQL pode concentrar a coordenação e reduzir chamadas específicas, desde que limites e resolvers sejam bem projetados. Para recursos que se encaixam em métodos HTTP e cache convencional, REST pode ser mais simples. Para serviços internos que precisam de contratos gerados ou streams contínuos, gRPC pode encaixar melhor. É possível usar mais de um estilo em partes diferentes de um produto; nenhum vence em todos os cenários.
Dicas e Boas Práticas
Modele o domínio antes do banco. Os tipos e campos devem representar o contrato útil para os clientes, não espelhar tabelas ou entidades internas. Isso deixa a API evoluir sem expor a estrutura de persistência.
Mantenha resolvers pequenos. Use-os para encaminhar argumentos a serviços e montar resultados, não para concentrar autorização, regra de negócio e consultas ad hoc. A lógica importante precisa ser reutilizável e testável fora do GraphQL.
Coloque limites em toda lista. Paginação com tamanho máximo evita que uma seleção válida peça um volume ilimitado de objetos. Combine esse limite com análise de profundidade ou custo quando o schema tiver relações amplas.
Trate N+1 como risco de acesso a dados. Meça quantas chamadas cada query dispara e use DataLoader quando vários resolvers pedirem chaves que possam ser agrupadas. Batching precisa respeitar tenant, autorização e escopo da requisição.
Valide e autorize mutations no domínio. Tipos GraphQL ajudam a rejeitar formatos inválidos, mas não substituem regras de negócio nem permissão por registro. Retorne erros úteis sem expor stack traces, SQL ou dados internos.
Evolua o schema de forma compatível. Prefira adicionar campos e deprecar os antigos com um prazo claro antes de removê-los. Acompanhe quais clientes ainda usam cada campo e trate mudanças incompatíveis como uma migração explícita.
Conclusão
GraphQL oferece ao cliente uma linguagem para selecionar dados dentro de um contrato tipado. Esse modelo é especialmente útil quando consumidores precisam de combinações diferentes de informações, mas exige atenção ao custo de cada operação, aos resolvers, à autorização e à evolução do schema. A implementação demonstrada usa GraphQL.NET 8.8.5 e o transporte ASP.NET Core 8.3.3 em .NET 10, sobre os mesmos produtos em memória que alimentam um endpoint REST.
Eu não escolheria GraphQL só porque uma query pode pedir poucos campos. REST também admite projeções e representações especializadas, e gRPC resolve outros problemas de contrato e streaming. A decisão deve partir dos clientes, dos fluxos de leitura e escrita, do cache, do transporte e da capacidade de operar a solução. Na próxima parte da série, vou tratar de SignalR como uma opção para comunicação em tempo real no .NET, sem confundir esse problema com consultas de dados.
Resumo Objetivo
- GraphQL é uma linguagem de consulta e um runtime que executa operações sobre um schema tipado; o cliente seleciona campos e o servidor valida e resolve cada operação.
- Query e Mutation são roots do schema GraphQL normalmente usados para leitura e escrita; mutations ainda precisam de validação de domínio, autorização e controle de efeitos colaterais.
- GraphQL.NET 8.8.5 e GraphQL.Server.Transports.AspNetCore 8.3.3 executaram query, mutation e validação HTTP em uma API ASP.NET Core/.NET 10 do sample BlogSamples.
- DataLoader pode agrupar buscas feitas por resolvers e reduzir o padrão N+1 quando a fonte de dados oferece uma operação em lote; ele não elimina a necessidade de limites ou autorização.
- REST pode oferecer projeções e representações especializadas; a diferença entre os JSONs deste artigo descreve apenas os contratos concretos testados.
- GraphQL, REST e gRPC têm modelos e custos operacionais diferentes; a escolha depende dos clientes, do contrato, do transporte e das necessidades de streaming e cache.
Leia Também
- gRPC sem mistério: o que é e quando usar no .NET 10
- Design de APIs REST: Verbos HTTP e Parameter Binding
- Tempo real no Angular e .NET: WebSockets, SSE e SignalR
Referências
- GraphQL Learn — introdução oficial a schemas, queries, mutations e práticas de operação.
- GraphQL Specification — September 2025 — semântica normativa da linguagem e do sistema de tipos.
- GraphQL.NET ASP.NET Core Integration — configuração atual do transporte e exemplo de integração com .NET 10.
- GraphQL.NET DataLoader — batching e cache por requisição para relacionamentos resolvidos por campo.
- GraphQL.NET Server — middleware ASP.NET Core, transporte HTTP e configuração de segurança.
- OWASP GraphQL Cheat Sheet — recomendações para validação, autorização, introspection e limites de consultas.
- GraphQL over HTTP — especificação em desenvolvimento para transportar operações GraphQL sobre HTTP. 📬
👉 Artigo completo com todos os exemplos de código: GraphQL para Iniciantes: Guia Completo com .NET
Top comments (0)