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.

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.

