GraphQL vs REST: Quando usar e quando evitar
Over-fetching e under-fetching na prática, o problema N+1 e como o DataLoader resolve, caching com persisted queries, schema tipado com resolvers, e quando REST ainda é a escolha certa.
O Facebook criou o GraphQL em 2012 para resolver um problema concreto: o app mobile do Facebook estava lenificando porque cada tela precisava de múltiplas chamadas REST para montar a UI — o feed, os amigos online, as notificações. Em conexões 3G lentas da época, cada round-trip adicional custava segundos de latência.
GraphQL não é substituto do REST — é uma solução para problemas específicos. Entender quando ele resolve e quando cria mais problemas do que resolve é a diferença entre uma decisão de arquitetura fundamentada e uma adoção por hype.
O Problema Central: Over-fetching e Under-fetching
A diferença fundamental é quem decide o shape da resposta. Em REST, o servidor decide: GET /users/1 retorna o que o endpoint foi programado para retornar. Em GraphQL, o cliente decide: a query especifica exatamente quais campos quer. Para um app com múltiplos clientes (mobile, web, TV, parceiros) com necessidades muito diferentes, o GraphQL elimina a proliferação de endpoints especializados. Para uma API simples usada por um único cliente, REST é mais simples de implementar, cachear e depurar.
- Over-fetching — a API retorna mais dados do que o cliente precisa.
GET /users/1retorna 40 campos, mas a tela de listagem usa apenasnameeavatar. Em mobile, isso desperdiça banda e bateria. - Under-fetching — a API não retorna dados suficientes, exigindo múltiplas requisições. Para montar a tela de perfil:
GET /users/1, depoisGET /users/1/posts, depoisGET /users/1/followers. São 3 round-trips que poderiam ser 1. - Múltiplos clientes com necessidades diferentes — o app mobile precisa de campos diferentes da versão web que precisa de campos diferentes do app de TV. Com REST, você cria endpoints especializados para cada um, ou todos sofrem de over-fetching.
Como o GraphQL Resolve
# O cliente pede EXATAMENTE o que precisa em uma única requisição
# Todos os campos são declarados explicitamente
query GetUserProfile($userId: ID!) {
user(id: $userId) {
id
name
avatar # Apenas estes 3 campos, não os 40 que o banco tem
# Relacionamentos resolvidos no mesmo round-trip
posts(limit: 3, orderBy: { createdAt: DESC }) {
id
title
likesCount
}
# Campos calculados pelo servidor
followersCount
isFollowedByMe
}
}Schema e Resolvers no Node.js
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
avatar: String
posts(limit: Int, offset: Int): [Post!]!
followersCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
likesCount: Int!
createdAt: String!
}
type Query {
user(id: ID!): User
users(page: Int, pageSize: Int): [User!]!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, content: String!): Post!
followUser(userId: ID!): Boolean!
}
`;import type { Resolvers } from './__generated__/resolvers-types';
export const resolvers: Resolvers = {
Query: {
user: async (_parent, { id }, context) => {
return context.usersRepo.findById(id);
},
users: async (_parent, { page = 1, pageSize = 20 }, context) => {
return context.usersRepo.findAll({ page, pageSize });
},
},
// Resolvers de campo: executados apenas se o campo for solicitado
User: {
// Este resolver só roda se o cliente pedir 'posts' no query
posts: async (user, { limit = 10, offset = 0 }, context) => {
return context.postsRepo.findByAuthor(user.id, { limit, offset });
},
// Contador calculado em tempo real
followersCount: async (user, _args, context) => {
return context.followsRepo.countFollowers(user.id);
},
},
Post: {
// Resolver de relacionamento — cuidado com o problema N+1 aqui!
author: async (post, _args, context) => {
// Sem DataLoader: N queries para N posts
// Com DataLoader: 1 query batched para todos os posts
return context.loaders.user.load(post.authorId);
},
},
Mutation: {
createPost: async (_parent, { title, content }, context) => {
if (!context.user) throw new Error('Não autenticado');
return context.postsRepo.create({ title, content, authorId: context.user.id });
},
},
};O Problema N+1 e DataLoader
O maior risco do GraphQL: quando o cliente pede uma lista de posts com os autores, o resolver Post.author é chamado uma vez por post. Com 100 posts, isso é 100 queries ao banco. O DataLoader resolve isso com batching: acumula todos os authorId solicitados e faz uma única query com WHERE id IN (...):
npm install dataloaderimport DataLoader from 'dataloader';
import type { IUsersRepository } from '../domain/repositories/IUsersRepository';
// Um DataLoader por tipo de entidade
export function createLoaders(usersRepo: IUsersRepository) {
return {
user: new DataLoader(async (userIds: readonly Array<string>) => {
// Uma única query para todos os IDs solicitados no mesmo tick
const users = await usersRepo.findByIds([...userIds]);
// O DataLoader exige que o resultado seja mapeado na mesma ordem dos IDs
const usersMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => usersMap.get(id) ?? new Error(`User ${id} not found`));
}),
};
}
// O DataLoader é criado POR REQUISIÇÃO (no contexto do Apollo Server)
// para evitar cache entre requisições de usuários diferentes:
// context: () => ({ ..., loaders: createLoaders(usersRepo) })Desvantagens Reais do GraphQL
- Cache HTTP — REST caches por URL (CDN, browser cache, HTTP headers). GraphQL usa um único endpoint
POST /graphql— o cache convencional não funciona. Solução: Persisted Queries (queries pré-registradas com ID), APQ (Automatic Persisted Queries) do Apollo. - Complexidade operacional — rate limiting por operação é difícil (uma query pode ser mais cara que outra). Depth limiting, complexity limiting e query whitelisting adicionam camadas de config.
- Superficie de ataque — um endpoint exposto que aceita queries arbitrárias pode ser alvo de denial-of-service via queries excessivamente complexas. Sem complexity limits, um cliente malicioso pode derrubar o servidor.
- Curva de aprendizado — schema, resolvers, DataLoader, N+1, context, field policies, code generation. REST é mais simples de entender e debugar.
- Over-engineering para CRUDs simples — se sua API é basicamente
GET/POST/PUT/DELETEem recursos simples com poucos clientes, REST é mais adequado.
Quando usar GraphQL: múltiplos clientes (web, mobile, TV) com necessidades diferentes de dados, APIs com muitos relacionamentos entre entidades, product teams que querem autonomia para compor seus próprios dados sem coordenar com o backend. Quando usar REST: APIs simples, APIs públicas consumidas por terceiros (REST é mais familiar), microserviços comunicando entre si, arquiteturas onde CDN cache é crítico.
Conclusão
GraphQL brilha em plataformas complexas com múltiplos clientes — é por isso que Facebook, GitHub, Shopify e Twitter o adotaram. Para projetos mais simples ou APIs públicas, REST é mais simples de implementar, debugar, cachear e documentar. A escolha não é ideológica — é sobre qual ferramenta resolve o problema específico com menos trade-offs.