Voltar para Artigos
Arquitetura9 min de leitura

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.

12 de agosto de 2026

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/1 retorna 40 campos, mas a tela de listagem usa apenas name e avatar. 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, depois GET /users/1/posts, depois GET /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

Query declarativa — cliente define o shape da resposta
# 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

typeDefs.ts
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!
  }
`;
resolvers.ts
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 (...):

bash
npm install dataloader
loaders.ts
import 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/DELETE em 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.