GraphQL na prática: schema, o problema N+1 e federation
GraphQL costuma ser vendido como o fim do REST, e essa promessa exagerada faz muita gente adotar a tecnologia pelos motivos errados. GraphQL não é melhor que REST; é diferente, com um conjunto próprio de forças e de armadilhas. Ele resolve de forma elegante um problema real (o cliente pedir exatamente os dados de que precisa, nem mais nem menos), mas cobra esse benefício em complexidade de servidor que, mal administrada, vira gargalo de performance e dor de manutenção.
Este guia é sobre a engenharia de GraphQL: como desenhar um schema que dura, o que os resolvers realmente fazem, o famoso problema N+1 e como resolvê-lo, e como a federation permite montar um grafo único a partir de vários serviços. O foco aqui é construir GraphQL bem, um assunto distinto de proteger uma API sob ataque ou versionar contratos REST.
O que GraphQL resolve de fato
Em uma API REST, cada endpoint devolve uma estrutura fixa. Para montar uma tela que precisa de dados de três recursos, o cliente costuma fazer três chamadas (underfetching), ou um único endpoint devolve muito mais do que a tela usa (overfetching). O GraphQL inverte o controle: existe um único endpoint, e o cliente descreve numa query exatamente o formato de dados que quer. O servidor responde nesse formato, sem sobra nem falta.
query {
pedido(id: "123") {
total
cliente { nome email }
itens { produto { nome } quantidade }
}
}
Uma única requisição traz o pedido, o cliente e os itens aninhados, no formato exato da tela. Esse é o ganho central: menos chamadas, menos dados trafegados e um contrato flexível que evolui sem quebrar telas existentes.
O schema é o contrato
O coração de uma API GraphQL é o schema: uma definição forte e tipada de todos os tipos de dados e operações disponíveis. Ele é, ao mesmo tempo, a documentação viva e o contrato entre cliente e servidor. Desenhar o schema com cuidado é a decisão mais importante de um projeto GraphQL.
type Pedido {
id: ID!
total: Float!
cliente: Cliente!
itens: [ItemPedido!]!
}
type Cliente {
id: ID!
nome: String!
pedidos: [Pedido!]!
}
Boas práticas de schema que separam um projeto saudável de uma bagunça:
- Modele por domínio, não por tela. O schema descreve o negócio, não uma interface específica. Telas mudam; o domínio é mais estável.
- Prefira tipos explícitos a genéricos. Um
status: StatusPedido(enum) comunica muito mais que umstatus: String. - Pense na evolução desde o início. GraphQL evolui adicionando campos e marcando os antigos como
@deprecated, sem versão nova na URL. Um campo bem nomeado hoje evita dívida amanhã. - Cuide da nulabilidade. O
!(não nulo) é uma promessa forte: se um campo obrigatório falha em resolver, ele pode derrubar partes maiores da resposta.
Resolvers: onde o schema vira dados
O schema diz o que existe; os resolvers dizem como obter cada pedaço. Cada campo do schema tem uma função resolver que sabe buscar aquele dado, seja no banco, em outro serviço ou em um cache. Quando uma query aninha campos, o GraphQL executa os resolvers em cascata: resolve o pedido, depois o cliente daquele pedido, depois os itens, e assim por diante.
Essa execução em cascata é elegante e é, também, exatamente onde nasce o maior problema de performance do GraphQL.
O problema N+1
Imagine uma query que pede uma lista de dez pedidos e, para cada um, o nome do cliente. O resolver de pedidos faz 1 consulta para trazer os dez. Depois, o resolver de cliente roda uma vez para cada pedido, fazendo 10 consultas adicionais, uma por cliente. Resultado: 1 + 10 = 11 consultas onde deveriam bastar 2. Com cem pedidos, são 101 consultas. Esse é o problema N+1, e ele degrada silenciosamente qualquer API GraphQL ingênua.
O N+1 não aparece em desenvolvimento com poucos dados. Ele explode em produção, sob volume real, quando o banco começa a receber uma enxurrada de consultas minúsculas e repetidas.
Dataloader: a solução
A cura padrão é o dataloader, um padrão de batching e caching por requisição. Em vez de disparar uma consulta por cliente na hora, o dataloader junta todos os pedidos de cliente daquela query e faz uma única consulta em lote (WHERE id IN (...)). Além disso, ele guarda em cache o resultado dentro da mesma requisição, então pedir o mesmo cliente duas vezes não consulta o banco duas vezes.
Com dataloader, aquela query de dez pedidos volta a custar 2 consultas em vez de 11. Ele é praticamente obrigatório em qualquer GraphQL de produção, e não um detalhe opcional de otimização.
Os cuidados que GraphQL exige
A flexibilidade de deixar o cliente montar a query tem um preço, e ignorá-lo abre brechas de performance e de segurança:
- Limite a profundidade e a complexidade das queries. Como o cliente monta a consulta, ele pode pedir aninhamentos absurdamente profundos que sobrecarregam o servidor. Imponha um teto de profundidade e um orçamento de complexidade.
- Cuidado com o cache de HTTP. REST cacheia respostas por URL com facilidade; em GraphQL, como tudo passa por um endpoint com corpo variável, o cache exige estratégia própria, geralmente no nível de campo ou de dataloader.
- Trate erros parciais. Uma query pode ter sucesso em parte e falhar em outra. O cliente precisa lidar com respostas que trazem dados e erros ao mesmo tempo.
Federation: um grafo a partir de muitos serviços
Em uma arquitetura com vários serviços, cada time é dono de um pedaço do domínio. A federation permite que cada serviço exponha seu próprio subgrafo de GraphQL, e um gateway os componha em um único grafo unificado para o cliente. O cliente faz uma query como se houvesse uma API só, enquanto o gateway distribui cada parte ao serviço dono e junta o resultado.
O ganho é organizacional: times evoluem seus subgrafos de forma independente, sem um schema monolítico gigante que todos precisam tocar. O custo é a operação de um gateway e a disciplina de definir bem as fronteiras e as chaves que ligam entidades entre serviços. Federation compensa quando a organização e o domínio já cresceram, e é overengineering quando você ainda tem um serviço só.
GraphQL ou REST?
A pergunta madura não é qual é superior, e sim qual encaixa no seu caso.
| Critério | GraphQL | REST |
|---|---|---|
| Formato da resposta | Definido pelo cliente | Fixo por endpoint |
| Over e underfetching | Resolvidos por design | Comuns, exigem ajuste |
| Cache HTTP | Exige estratégia própria | Simples, por URL |
| Curva de complexidade no servidor | Alta (resolvers, N+1) | Baixa |
| Ideal para | Clientes ricos, muitos dados relacionados | Recursos simples, integrações públicas |
GraphQL brilha quando há muitos tipos de dados relacionados e clientes variados (web, mobile) que precisam de recortes diferentes dos mesmos dados. REST continua sendo a escolha mais simples e robusta para recursos diretos, APIs públicas e integrações em que a previsibilidade e o cache fácil valem mais que a flexibilidade. Muitos sistemas maduros usam os dois: GraphQL na fronteira com os apps, REST em integrações internas e públicas.
Adote pela dor certa
GraphQL resolve problemas reais de over e underfetching e dá aos clientes um contrato flexível e tipado. Em troca, transfere complexidade para o servidor: schema para desenhar, resolvers para orquestrar, N+1 para domar com dataloader, profundidade para limitar. Adotado pela dor certa, com um schema bem modelado e dataloader desde o dia 1, ele é uma ferramenta poderosa. Adotado por moda, vira uma camada frágil e lenta sobre um problema que REST resolveria com metade do esforço.
Está avaliando GraphQL ou lutando com uma API que ficou lenta? Na ConstructorTI, desenhamos APIs GraphQL com schema forte, dataloader contra N+1 e federation quando o domínio pede, ou mantemos REST quando ele é a escolha mais simples. Se a sua camada de API virou gargalo de performance ou de manutenção, fale com a nossa engenharia e receba um diagnóstico técnico do seu contrato de dados.