GraphQL

GraphQL

GraphQL foi criado pelo Facebook em 2012 e open-sourced em 2015. Surgiu de um problema real: o app mobile do Facebook fazia dezenas de requests REST para montar uma única tela, buscando dados demais (over-fetching) ou de menos (under-fetching). GraphQL resolve isso permitindo que o cliente especifique exatamente os dados que precisa em uma única query.


1. Conceitos Fundamentais

1.1 Schema Definition Language (SDL)

O schema é o contrato da API — define os tipos, as queries possíveis e as mutations disponíveis:

# Types definem a estrutura dos dados
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!      # Relação 1:N
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!         # Relação N:1
  comments: [Comment!]!
  published: Boolean!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

# Queries disponíveis (leitura)
type Query {
  user(id: ID!): User
  users(limit: Int = 10, offset: Int = 0): [User!]!
  post(id: ID!): Post
  feed(cursor: String, limit: Int = 20): FeedConnection!
}

# Mutations disponíveis (escrita)
type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
}

# Input types para mutations
input CreatePostInput {
  title: String!
  content: String!
  published: Boolean = false
}

input UpdatePostInput {
  title: String
  content: String
  published: Boolean
}

1.2 Queries: O Cliente Decide

# O cliente pede EXATAMENTE o que precisa
query {
  user(id: "123") {
    name
    email
    posts {
      title
      published
    }
  }
}

# Resposta: apenas os campos solicitados
{
  "data": {
    "user": {
      "name": "Maria",
      "email": "maria@example.com",
      "posts": [
        { "title": "GraphQL na Prática", "published": true },
        { "title": "Rascunho", "published": false }
      ]
    }
  }
}

Compare com REST: para obter o mesmo dado você precisaria de GET /users/123 + GET /users/123/posts, ou de um endpoint custom que retorna tudo (incluindo campos que você não precisa).


2. Resolvers

Resolvers são funções que retornam dados para cada campo do schema:

const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      return context.db.users.findById(id);
    },
    users: async (_, { limit, offset }, context) => {
      return context.db.users.findAll({ limit, offset });
    },
  },

  // Resolver para campo relacional
  User: {
    posts: async (parent, _, context) => {
      // parent.id é o User já resolvido pela query acima
      return context.db.posts.findByAuthorId(parent.id);
    },
  },

  Mutation: {
    createPost: async (_, { input }, context) => {
      // Verificação de autenticação
      if (!context.user) throw new AuthenticationError('Não autenticado');

      return context.db.posts.create({
        ...input,
        authorId: context.user.id,
      });
    },
  },
};

2.1 O Problema N+1 em GraphQL

query {
  users(limit: 10) {  # 1 query: SELECT * FROM users LIMIT 10
    name
    posts {            # N queries: SELECT * FROM posts WHERE author_id = ?
      title            # (uma para cada usuário!)
    }
  }
}
# Total: 1 + 10 = 11 queries!

2.2 DataLoader: A Solução

DataLoader (Facebook, 2016) agrupa e cacheia chamadas dentro de um único tick do event loop:

import DataLoader from 'dataloader';

// Crie um novo DataLoader POR REQUEST (não global!)
function createLoaders(db) {
  return {
    postsByAuthor: new DataLoader(async (authorIds) => {
      // Uma única query batch
      const posts = await db.posts.findAll({
        where: { authorId: authorIds },
      });

      // Mapear resultados de volta para a ordem dos IDs
      return authorIds.map(id =>
        posts.filter(post => post.authorId === id)
      );
    }),
  };
}

// No resolver, use o loader em vez de query direta
const resolvers = {
  User: {
    posts: (parent, _, context) => {
      return context.loaders.postsByAuthor.load(parent.id);
      // 10 chamadas .load() no mesmo tick → 1 query batch
    },
  },
};

3. Subscriptions: Real-time com GraphQL

# Schema
type Subscription {
  messageAdded(roomId: ID!): Message!
  userTyping(roomId: ID!): User!
}
// Servidor (com graphql-subscriptions)
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();

const resolvers = {
  Mutation: {
    sendMessage: async (_, { roomId, content }, context) => {
      const message = await db.messages.create({
        roomId, content, authorId: context.user.id,
      });

      // Publica evento para subscribers
      pubsub.publish(`MESSAGE_ADDED_${roomId}`, {
        messageAdded: message,
      });

      return message;
    },
  },

  Subscription: {
    messageAdded: {
      subscribe: (_, { roomId }) => {
        return pubsub.asyncIterator(`MESSAGE_ADDED_${roomId}`);
      },
    },
  },
};

4. Paginação Cursor-Based

type FeedConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

# Query
query {
  feed(first: 10, after: "cursor_abc") {
    edges {
      cursor
      node {
        title
        author { name }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

5. Segurança em GraphQL

5.1 Depth Limiting

// Previne queries profundas demais (DoS)
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  validationRules: [depthLimit(7)],
});

// Bloqueia:
// query { user { posts { comments { author { posts { comments { ... } } } } } } }

5.2 Query Complexity

// Atribui custo a cada campo e limita o custo total
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';

const rule = createComplexityRule({
  maximumComplexity: 1000,
  estimators: [
    simpleEstimator({ defaultComplexity: 1 }),
  ],
  onComplete: (complexity) => {
    console.log('Query complexity:', complexity);
  },
});

5.3 Persisted Queries

Em produção, o cliente não envia a query completa — envia apenas um hash. O servidor mantém um mapa hash → query aprovada. Isso previne queries maliciosas e reduz bandwidth.


6. Federation: Múltiplos Serviços

Apollo Federation permite que cada equipe mantenha seu próprio subgraph, compostos em um supergraph unificado:

# Subgraph: Users Service
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}

# Subgraph: Posts Service
type User @key(fields: "id") {
  id: ID!
  posts: [Post!]!  # Estende User com posts
}

type Post @key(fields: "id") {
  id: ID!
  title: String!
  content: String!
  author: User!
}

# O Gateway compõe ambos em um schema unificado
# O cliente não sabe que são serviços diferentes

7. GraphQL vs REST: Trade-offs

AspectoRESTGraphQL
Over-fetchingComum (endpoints fixos)Eliminado (cliente escolhe campos)
Under-fetchingMúltiplos requestsUma query resolve
CachingHTTP cache nativo (CDN, ETag)Complexo (precisa de client-side cache)
VersionamentoURLs versionadas (/v1, /v2)Evolução do schema (deprecation)
ToolingSwagger/OpenAPIIntrospecção nativa, GraphiQL
Curva de aprendizadoBaixaMédia-alta
Melhor paraAPIs públicas, CRUD simplesFrontends complexos, mobile, micro-frontends

Regra prática: use REST para APIs públicas simples e cenários onde HTTP caching é crítico. Use GraphQL quando o frontend tem queries variadas e precisa de flexibilidade para compor dados de múltiplas fontes.


8. Referências e Aprofundamento

  • graphql.org — especificação oficial e tutorial
  • “Production Ready GraphQL” (Marc-André Giroux) — patterns e anti-patterns para produção
  • Apollo Documentation — servidor, cliente e federation
  • DataLoader (GitHub) — implementação de referência do Facebook
  • “GraphQL in Action” (Samer Buna) — guia prático end-to-end