Voltar para o Blog

GraphQL em 2025: Guia Completo com Apollo Server Para APIs Otimizadas

Ola HaWkers, GraphQL continua ganhando adocao em 2025 como uma alternativa poderosa ao REST tradicional. Se voce ainda nao explorou essa tecnologia ou quer aprofundar seus conhecimentos, este e o momento ideal.

Ja se frustrou com APIs REST que retornam dados demais ou de menos? GraphQL resolve exatamente esse problema, permitindo que o cliente solicite apenas os dados que precisa.

Por Que GraphQL em 2025

GraphQL nao veio para substituir REST completamente, mas oferece vantagens significativas em cenarios especificos.

GraphQL

Vantagens do GraphQL

Para o Cliente:

  • Solicita exatamente os dados necessarios
  • Uma unica requisicao para dados relacionados
  • Tipagem forte e documentacao automatica
  • Introspecao do schema

Para o Servidor:

  • Schema como contrato de API
  • Validacao automatica de queries
  • Evolucao da API sem versionamento
  • Melhor observabilidade de uso

Quando Usar GraphQL

Use GraphQL quando:

  • Clientes diferentes precisam de dados diferentes
  • Multiplos recursos relacionados sao necessarios
  • Performance de rede e critica (mobile)
  • API publica com muitos consumidores

Prefira REST quando:

  • Operacoes CRUD simples
  • Caching HTTP e prioritario
  • Equipe sem experiencia com GraphQL
  • Integracao com sistemas legados

Configurando Apollo Server

Vamos criar uma API GraphQL completa do zero com Apollo Server 4 e TypeScript.

Setup Inicial

// src/index.ts
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
import http from 'http';
import cors from 'cors';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { createContext, Context } from './context';

async function startServer() {
  const app = express();
  const httpServer = http.createServer(app);

  const server = new ApolloServer<Context>({
    typeDefs,
    resolvers,
    plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
    introspection: process.env.NODE_ENV !== 'production'
  });

  await server.start();

  app.use(
    '/graphql',
    cors<cors.CorsRequest>(),
    express.json(),
    expressMiddleware(server, {
      context: createContext
    })
  );

  const PORT = process.env.PORT || 4000;

  await new Promise<void>((resolve) =>
    httpServer.listen({ port: PORT }, resolve)
  );

  console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
}

startServer().catch(console.error);

Definindo o Schema

// src/schema.ts
export const typeDefs = `#graphql
  # Tipos basicos
  type Usuario {
    id: ID!
    nome: String!
    email: String!
    avatar: String
    posts: [Post!]!
    comentarios: [Comentario!]!
    criadoEm: String!
    atualizadoEm: String!
  }

  type Post {
    id: ID!
    titulo: String!
    conteudo: String!
    publicado: Boolean!
    autor: Usuario!
    categorias: [Categoria!]!
    comentarios: [Comentario!]!
    visualizacoes: Int!
    criadoEm: String!
    atualizadoEm: String!
  }

  type Categoria {
    id: ID!
    nome: String!
    slug: String!
    posts: [Post!]!
  }

  type Comentario {
    id: ID!
    conteudo: String!
    autor: Usuario!
    post: Post!
    criadoEm: String!
  }

  # Inputs para mutations
  input CriarUsuarioInput {
    nome: String!
    email: String!
    senha: String!
    avatar: String
  }

  input AtualizarUsuarioInput {
    nome: String
    avatar: String
  }

  input CriarPostInput {
    titulo: String!
    conteudo: String!
    publicado: Boolean = false
    categoriaIds: [ID!]
  }

  input AtualizarPostInput {
    titulo: String
    conteudo: String
    publicado: Boolean
    categoriaIds: [ID!]
  }

  input FiltroPost {
    publicado: Boolean
    autorId: ID
    categoriaId: ID
    busca: String
  }

  # Paginacao
  type PaginacaoInfo {
    total: Int!
    pagina: Int!
    porPagina: Int!
    totalPaginas: Int!
    temProxima: Boolean!
    temAnterior: Boolean!
  }

  type PostsPaginados {
    dados: [Post!]!
    paginacao: PaginacaoInfo!
  }

  # Queries
  type Query {
    # Usuarios
    usuarios: [Usuario!]!
    usuario(id: ID!): Usuario
    usuarioPorEmail(email: String!): Usuario

    # Posts
    posts(
      filtro: FiltroPost
      pagina: Int = 1
      porPagina: Int = 10
    ): PostsPaginados!
    post(id: ID!): Post

    # Categorias
    categorias: [Categoria!]!
    categoria(slug: String!): Categoria
  }

  # Mutations
  type Mutation {
    # Usuarios
    criarUsuario(input: CriarUsuarioInput!): Usuario!
    atualizarUsuario(id: ID!, input: AtualizarUsuarioInput!): Usuario!
    deletarUsuario(id: ID!): Boolean!

    # Posts
    criarPost(input: CriarPostInput!): Post!
    atualizarPost(id: ID!, input: AtualizarPostInput!): Post!
    deletarPost(id: ID!): Boolean!
    publicarPost(id: ID!): Post!

    # Comentarios
    criarComentario(postId: ID!, conteudo: String!): Comentario!
    deletarComentario(id: ID!): Boolean!
  }

  # Subscriptions (opcional)
  type Subscription {
    novoComentario(postId: ID!): Comentario!
    postAtualizado(id: ID!): Post!
  }
`;

Implementando Resolvers

// src/resolvers/index.ts
import { usuarioResolvers } from './usuario';
import { postResolvers } from './post';
import { categoriaResolvers } from './categoria';
import { comentarioResolvers } from './comentario';

export const resolvers = {
  Query: {
    ...usuarioResolvers.Query,
    ...postResolvers.Query,
    ...categoriaResolvers.Query
  },
  Mutation: {
    ...usuarioResolvers.Mutation,
    ...postResolvers.Mutation,
    ...comentarioResolvers.Mutation
  },
  // Field resolvers
  Usuario: usuarioResolvers.Usuario,
  Post: postResolvers.Post,
  Categoria: categoriaResolvers.Categoria,
  Comentario: comentarioResolvers.Comentario
};

// src/resolvers/post.ts
import { Context } from '../context';
import { GraphQLError } from 'graphql';

interface FiltroPost {
  publicado?: boolean;
  autorId?: string;
  categoriaId?: string;
  busca?: string;
}

export const postResolvers = {
  Query: {
    posts: async (
      _: unknown,
      args: { filtro?: FiltroPost; pagina?: number; porPagina?: number },
      { db, usuario }: Context
    ) => {
      const { filtro = {}, pagina = 1, porPagina = 10 } = args;
      const offset = (pagina - 1) * porPagina;

      // Construir query dinamica
      let where: any = {};

      if (filtro.publicado !== undefined) {
        where.publicado = filtro.publicado;
      }

      if (filtro.autorId) {
        where.autorId = filtro.autorId;
      }

      if (filtro.busca) {
        where.OR = [
          { titulo: { contains: filtro.busca, mode: 'insensitive' } },
          { conteudo: { contains: filtro.busca, mode: 'insensitive' } }
        ];
      }

      const [posts, total] = await Promise.all([
        db.post.findMany({
          where,
          skip: offset,
          take: porPagina,
          orderBy: { criadoEm: 'desc' }
        }),
        db.post.count({ where })
      ]);

      const totalPaginas = Math.ceil(total / porPagina);

      return {
        dados: posts,
        paginacao: {
          total,
          pagina,
          porPagina,
          totalPaginas,
          temProxima: pagina < totalPaginas,
          temAnterior: pagina > 1
        }
      };
    },

    post: async (_: unknown, { id }: { id: string }, { db }: Context) => {
      const post = await db.post.findUnique({ where: { id } });

      if (!post) {
        throw new GraphQLError('Post nao encontrado', {
          extensions: { code: 'NOT_FOUND' }
        });
      }

      // Incrementar visualizacoes
      await db.post.update({
        where: { id },
        data: { visualizacoes: { increment: 1 } }
      });

      return post;
    }
  },

  Mutation: {
    criarPost: async (
      _: unknown,
      { input }: { input: any },
      { db, usuario }: Context
    ) => {
      if (!usuario) {
        throw new GraphQLError('Nao autorizado', {
          extensions: { code: 'UNAUTHORIZED' }
        });
      }

      const { categoriaIds, ...dados } = input;

      return db.post.create({
        data: {
          ...dados,
          autorId: usuario.id,
          categorias: categoriaIds
            ? { connect: categoriaIds.map((id: string) => ({ id })) }
            : undefined
        }
      });
    },

    atualizarPost: async (
      _: unknown,
      { id, input }: { id: string; input: any },
      { db, usuario }: Context
    ) => {
      if (!usuario) {
        throw new GraphQLError('Nao autorizado', {
          extensions: { code: 'UNAUTHORIZED' }
        });
      }

      const post = await db.post.findUnique({ where: { id } });

      if (!post) {
        throw new GraphQLError('Post nao encontrado', {
          extensions: { code: 'NOT_FOUND' }
        });
      }

      if (post.autorId !== usuario.id) {
        throw new GraphQLError('Sem permissao para editar este post', {
          extensions: { code: 'FORBIDDEN' }
        });
      }

      const { categoriaIds, ...dados } = input;

      return db.post.update({
        where: { id },
        data: {
          ...dados,
          categorias: categoriaIds
            ? { set: categoriaIds.map((catId: string) => ({ id: catId })) }
            : undefined,
          atualizadoEm: new Date()
        }
      });
    },

    publicarPost: async (
      _: unknown,
      { id }: { id: string },
      { db, usuario }: Context
    ) => {
      if (!usuario) {
        throw new GraphQLError('Nao autorizado', {
          extensions: { code: 'UNAUTHORIZED' }
        });
      }

      return db.post.update({
        where: { id },
        data: { publicado: true, atualizadoEm: new Date() }
      });
    }
  },

  // Field resolvers para relacionamentos
  Post: {
    autor: (post: any, _: unknown, { loaders }: Context) => {
      return loaders.usuario.load(post.autorId);
    },

    categorias: (post: any, _: unknown, { db }: Context) => {
      return db.categoria.findMany({
        where: { posts: { some: { id: post.id } } }
      });
    },

    comentarios: (post: any, _: unknown, { db }: Context) => {
      return db.comentario.findMany({
        where: { postId: post.id },
        orderBy: { criadoEm: 'desc' }
      });
    }
  }
};

DataLoader: Evitando N+1

O problema N+1 e um dos maiores viloes de performance em GraphQL. DataLoader resolve isso com batching.

// src/loaders.ts
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';

export function createLoaders(db: PrismaClient) {
  return {
    usuario: new DataLoader(async (ids: readonly string[]) => {
      const usuarios = await db.usuario.findMany({
        where: { id: { in: [...ids] } }
      });

      // Mapear para manter a ordem
      const usuarioMap = new Map(usuarios.map((u) => [u.id, u]));
      return ids.map((id) => usuarioMap.get(id) || null);
    }),

    post: new DataLoader(async (ids: readonly string[]) => {
      const posts = await db.post.findMany({
        where: { id: { in: [...ids] } }
      });

      const postMap = new Map(posts.map((p) => [p.id, p]));
      return ids.map((id) => postMap.get(id) || null);
    }),

    // Loader para posts por autor (batch por autorId)
    postsPorAutor: new DataLoader(async (autorIds: readonly string[]) => {
      const posts = await db.post.findMany({
        where: { autorId: { in: [...autorIds] } }
      });

      // Agrupar por autorId
      const postsPorAutor = new Map<string, any[]>();
      posts.forEach((post) => {
        const lista = postsPorAutor.get(post.autorId) || [];
        lista.push(post);
        postsPorAutor.set(post.autorId, lista);
      });

      return autorIds.map((id) => postsPorAutor.get(id) || []);
    }),

    // Loader para contagem de comentarios
    contagemComentarios: new DataLoader(async (postIds: readonly string[]) => {
      const contagens = await db.comentario.groupBy({
        by: ['postId'],
        where: { postId: { in: [...postIds] } },
        _count: { id: true }
      });

      const contagemMap = new Map(
        contagens.map((c) => [c.postId, c._count.id])
      );

      return postIds.map((id) => contagemMap.get(id) || 0);
    })
  };
}

// src/context.ts
import { PrismaClient } from '@prisma/client';
import { createLoaders } from './loaders';
import { verificarToken } from './auth';

const db = new PrismaClient();

export interface Context {
  db: PrismaClient;
  loaders: ReturnType<typeof createLoaders>;
  usuario: { id: string; email: string } | null;
}

export async function createContext({ req }: { req: any }): Promise<Context> {
  const token = req.headers.authorization?.replace('Bearer ', '');
  const usuario = token ? await verificarToken(token) : null;

  return {
    db,
    loaders: createLoaders(db), // Novos loaders por requisicao
    usuario
  };
}

Queries e Mutations na Pratica

Vejamos como usar a API do lado do cliente.

Queries

# Buscar lista de posts paginada
query ListarPosts($filtro: FiltroPost, $pagina: Int) {
  posts(filtro: $filtro, pagina: $pagina, porPagina: 10) {
    dados {
      id
      titulo
      conteudo
      publicado
      visualizacoes
      criadoEm
      autor {
        id
        nome
        avatar
      }
      categorias {
        id
        nome
        slug
      }
    }
    paginacao {
      total
      pagina
      totalPaginas
      temProxima
      temAnterior
    }
  }
}

# Buscar post com todos os detalhes
query BuscarPost($id: ID!) {
  post(id: $id) {
    id
    titulo
    conteudo
    publicado
    visualizacoes
    criadoEm
    atualizadoEm
    autor {
      id
      nome
      email
      avatar
    }
    categorias {
      id
      nome
      slug
    }
    comentarios {
      id
      conteudo
      criadoEm
      autor {
        id
        nome
        avatar
      }
    }
  }
}

# Buscar usuario com seus posts
query PerfilUsuario($id: ID!) {
  usuario(id: $id) {
    id
    nome
    email
    avatar
    criadoEm
    posts {
      id
      titulo
      publicado
      visualizacoes
      criadoEm
    }
  }
}

Mutations

# Criar novo post
mutation CriarPost($input: CriarPostInput!) {
  criarPost(input: $input) {
    id
    titulo
    conteudo
    publicado
    criadoEm
  }
}

# Atualizar post existente
mutation AtualizarPost($id: ID!, $input: AtualizarPostInput!) {
  atualizarPost(id: $id, input: $input) {
    id
    titulo
    conteudo
    publicado
    atualizadoEm
  }
}

# Publicar post
mutation PublicarPost($id: ID!) {
  publicarPost(id: $id) {
    id
    publicado
    atualizadoEm
  }
}

# Adicionar comentario
mutation AdicionarComentario($postId: ID!, $conteudo: String!) {
  criarComentario(postId: $postId, conteudo: $conteudo) {
    id
    conteudo
    criadoEm
    autor {
      id
      nome
    }
  }
}

Otimizacoes Avancadas

Persisted Queries

Para producao, persisted queries melhoram seguranca e performance:

// src/plugins/persistedQueries.ts
import { ApolloServerPlugin } from '@apollo/server';
import { createHash } from 'crypto';

const queryStore = new Map<string, string>();

export const persistedQueriesPlugin: ApolloServerPlugin = {
  async requestDidStart() {
    return {
      async didResolveOperation({ request, document }) {
        if (request.extensions?.persistedQuery) {
          const { sha256Hash } = request.extensions.persistedQuery;

          // Verificar se query esta registrada
          if (!queryStore.has(sha256Hash)) {
            if (request.query) {
              // Registrar nova query
              const hash = createHash('sha256')
                .update(request.query)
                .digest('hex');

              if (hash === sha256Hash) {
                queryStore.set(sha256Hash, request.query);
              }
            }
          }
        }
      }
    };
  }
};

Query Complexity

Limitar a complexidade das queries evita abusos:

// src/plugins/queryComplexity.ts
import { GraphQLError } from 'graphql';
import {
  fieldExtensionsEstimator,
  getComplexity,
  simpleEstimator
} from 'graphql-query-complexity';

const MAX_COMPLEXITY = 1000;

export function validateQueryComplexity(schema: any, query: any, variables: any) {
  const complexity = getComplexity({
    schema,
    query,
    variables,
    estimators: [
      fieldExtensionsEstimator(),
      simpleEstimator({ defaultComplexity: 1 })
    ]
  });

  if (complexity > MAX_COMPLEXITY) {
    throw new GraphQLError(
      `Query muito complexa: ${complexity}. Maximo permitido: ${MAX_COMPLEXITY}`,
      { extensions: { code: 'QUERY_TOO_COMPLEX' } }
    );
  }

  return complexity;
}

Caching com Redis

// src/plugins/caching.ts
import Redis from 'ioredis';
import { createHash } from 'crypto';

const redis = new Redis(process.env.REDIS_URL);

export async function cacheResolver<T>(
  key: string,
  ttl: number,
  resolver: () => Promise<T>
): Promise<T> {
  const cached = await redis.get(key);

  if (cached) {
    return JSON.parse(cached);
  }

  const result = await resolver();

  await redis.setex(key, ttl, JSON.stringify(result));

  return result;
}

// Uso nos resolvers
posts: async (_: unknown, args: any, { db }: Context) => {
  const cacheKey = `posts:${createHash('md5').update(JSON.stringify(args)).digest('hex')}`;

  return cacheResolver(cacheKey, 300, async () => {
    return db.post.findMany({ /* ... */ });
  });
}

Tratamento de Erros

// src/errors.ts
import { GraphQLError } from 'graphql';

export class ErroValidacao extends GraphQLError {
  constructor(mensagem: string, campo?: string) {
    super(mensagem, {
      extensions: {
        code: 'VALIDATION_ERROR',
        campo
      }
    });
  }
}

export class ErroAutenticacao extends GraphQLError {
  constructor(mensagem = 'Nao autorizado') {
    super(mensagem, {
      extensions: { code: 'UNAUTHENTICATED' }
    });
  }
}

export class ErroPermissao extends GraphQLError {
  constructor(mensagem = 'Sem permissao') {
    super(mensagem, {
      extensions: { code: 'FORBIDDEN' }
    });
  }
}

export class ErroNaoEncontrado extends GraphQLError {
  constructor(recurso: string) {
    super(`${recurso} nao encontrado`, {
      extensions: { code: 'NOT_FOUND' }
    });
  }
}

Se voce quer aprender mais sobre arquiteturas de backend modernas, confira o artigo Arquitetura Serverless com JavaScript que complementa muito bem o GraphQL.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário