GraphQL vs REST em 2025: Por Que APIs Estão Mudando
Olá HaWkers, a guerra entre GraphQL e REST APIs está longe de terminar, mas em 2025, o panorama ficou muito mais claro. Empresas como Netflix, Shopify, GitHub e Airbnb migraram partes significativas de suas APIs para GraphQL - e há razões concretas para isso.
Mas aqui está o twist: REST não está morto. Na verdade, para muitos casos de uso, REST continua sendo a escolha mais inteligente. Então, como saber qual usar? E mais importante: como implementar cada um de forma eficiente em Node.js?
Vamos mergulhar fundo nessa comparação com exemplos práticos que você pode usar hoje.
REST: O Clássico Que Ainda Domina
REST (Representational State Transfer) é o padrão estabelecido há décadas. Sua simplicidade e previsibilidade são suas maiores forças:
// API REST tradicional com Express.js
import express from 'express';
import { body, validationResult } from 'express-validator';
const app = express();
app.use(express.json());
// In-memory database (em produção, use um banco real)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', posts: [1, 2] },
{ id: 2, name: 'Bob', email: 'bob@example.com', posts: [3] }
];
let posts = [
{ id: 1, title: 'GraphQL Intro', authorId: 1, comments: [1] },
{ id: 2, title: 'REST Best Practices', authorId: 1, comments: [] },
{ id: 3, title: 'Node.js Performance', authorId: 2, comments: [2, 3] }
];
let comments = [
{ id: 1, text: 'Great article!', postId: 1, authorId: 2 },
{ id: 2, text: 'Very helpful', postId: 3, authorId: 1 },
{ id: 3, text: 'Thanks for sharing', postId: 3, authorId: 1 }
];
// GET /api/users - Listar todos os usuários
app.get('/api/users', (req, res) => {
const { limit = 10, offset = 0 } = req.query;
const paginatedUsers = users.slice(
parseInt(offset),
parseInt(offset) + parseInt(limit)
);
res.json({
data: paginatedUsers,
pagination: {
total: users.length,
limit: parseInt(limit),
offset: parseInt(offset)
}
});
});
// GET /api/users/:id - Buscar usuário específico
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
});
// GET /api/users/:id/posts - Posts de um usuário
app.get('/api/users/:id/posts', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const userPosts = posts.filter(p =>
user.posts.includes(p.id)
);
res.json({ data: userPosts });
});
// GET /api/posts/:id/comments - Comentários de um post
app.get('/api/posts/:id/comments', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const postComments = comments.filter(c =>
post.comments.includes(c.id)
);
res.json({ data: postComments });
});
// POST /api/users - Criar novo usuário
app.post(
'/api/users',
[
body('name').trim().isLength({ min: 2 }).escape(),
body('email').isEmail().normalizeEmail()
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
posts: []
};
users.push(newUser);
res.status(201).json({ data: newUser });
}
);
// PUT /api/users/:id - Atualizar usuário
app.put('/api/users/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[userIndex] = {
...users[userIndex],
...req.body,
id: users[userIndex].id // Manter o ID original
};
res.json({ data: users[userIndex] });
});
// DELETE /api/users/:id - Deletar usuário
app.delete('/api/users/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(userIndex, 1);
res.status(204).send();
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`REST API running on port ${PORT}`);
});
O problema clássico do REST? Over-fetching e under-fetching. Para obter um usuário com seus posts e comentários, você precisa fazer múltiplas requisições:
// Cliente consumindo a REST API - problema N+1
async function getUserWithPostsAndComments(userId) {
// Request 1: Buscar usuário
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Request 2: Buscar posts do usuário
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const userPosts = await postsResponse.json();
// Requests 3-N: Buscar comentários de cada post
const postsWithComments = await Promise.all(
userPosts.data.map(async (post) => {
const commentsResponse = await fetch(`/api/posts/${post.id}/comments`);
const comments = await commentsResponse.json();
return { ...post, comments: comments.data };
})
);
return {
...user.data,
posts: postsWithComments
};
}
// 1 + 1 + N requisições = Problema de performance!
GraphQL: Flexibilidade e Eficiência
GraphQL resolve exatamente esse problema. Com uma única query, você busca exatamente os dados que precisa:
// API GraphQL com Apollo Server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Definir schema GraphQL
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
post: Post!
author: User!
}
type Query {
users(limit: Int, offset: Int): [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, authorId: ID!): Post!
createComment(text: String!, postId: ID!, authorId: ID!): Comment!
}
`;
// Mesmos dados do exemplo REST
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', postIds: [1, 2] },
{ id: 2, name: 'Bob', email: 'bob@example.com', postIds: [3] }
];
let posts = [
{ id: 1, title: 'GraphQL Intro', authorId: 1, commentIds: [1] },
{ id: 2, title: 'REST Best Practices', authorId: 1, commentIds: [] },
{ id: 3, title: 'Node.js Performance', authorId: 2, commentIds: [2, 3] }
];
let comments = [
{ id: 1, text: 'Great article!', postId: 1, authorId: 2 },
{ id: 2, text: 'Very helpful', postId: 3, authorId: 1 },
{ id: 3, text: 'Thanks for sharing', postId: 3, authorId: 1 }
];
// Resolvers - como buscar os dados
const resolvers = {
Query: {
users: (_, { limit = 10, offset = 0 }) => {
return users.slice(offset, offset + limit);
},
user: (_, { id }) => {
return users.find(u => u.id === parseInt(id));
},
posts: () => posts,
post: (_, { id }) => {
return posts.find(p => p.id === parseInt(id));
}
},
User: {
posts: (parent) => {
return posts.filter(p => parent.postIds.includes(p.id));
}
},
Post: {
author: (parent) => {
return users.find(u => u.id === parent.authorId);
},
comments: (parent) => {
return comments.filter(c => parent.commentIds.includes(c.id));
}
},
Comment: {
post: (parent) => {
return posts.find(p => p.id === parent.postId);
},
author: (parent) => {
return users.find(u => u.id === parent.authorId);
}
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = {
id: users.length + 1,
name,
email,
postIds: []
};
users.push(newUser);
return newUser;
},
updateUser: (_, { id, name, email }) => {
const userIndex = users.findIndex(u => u.id === parseInt(id));
if (userIndex === -1) {
throw new Error('User not found');
}
users[userIndex] = {
...users[userIndex],
...(name && { name }),
...(email && { email })
};
return users[userIndex];
},
deleteUser: (_, { id }) => {
const userIndex = users.findIndex(u => u.id === parseInt(id));
if (userIndex === -1) return false;
users.splice(userIndex, 1);
return true;
},
createPost: (_, { title, authorId }) => {
const newPost = {
id: posts.length + 1,
title,
authorId: parseInt(authorId),
commentIds: []
};
posts.push(newPost);
// Adicionar post ao usuário
const user = users.find(u => u.id === parseInt(authorId));
if (user) {
user.postIds.push(newPost.id);
}
return newPost;
},
createComment: (_, { text, postId, authorId }) => {
const newComment = {
id: comments.length + 1,
text,
postId: parseInt(postId),
authorId: parseInt(authorId)
};
comments.push(newComment);
// Adicionar comentário ao post
const post = posts.find(p => p.id === parseInt(postId));
if (post) {
post.commentIds.push(newComment.id);
}
return newComment;
}
}
};
// Criar servidor Apollo
const server = new ApolloServer({
typeDefs,
resolvers
});
// Iniciar servidor
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`GraphQL server running at ${url}`);
Agora, do lado do cliente, uma única query resolve tudo:
// Cliente GraphQL - UMA requisição!
const GET_USER_WITH_DATA = `
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
comments {
id
text
author {
name
}
}
}
}
}
`;
async function getUserWithPostsAndComments(userId) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: GET_USER_WITH_DATA,
variables: { userId }
})
});
const { data } = await response.json();
return data.user;
}
// Apenas 1 requisição - muito mais eficiente!
const user = await getUserWithPostsAndComments('1');
console.log(user);
Quando Usar REST vs GraphQL
A escolha não é binária. Aqui está um guia prático:
Use REST quando:
- API pública simples: APIs públicas com poucos endpoints (ex: webhooks)
- Caching é crítico: REST se beneficia de HTTP caching (CDNs, browsers)
- Time pequeno: Menos complexidade para manter
- CRUD simples: Operações básicas sem relacionamentos complexos
- Performance previsível: Latência e throughput consistentes
Use GraphQL quando:
- Múltiplos clientes: Mobile, web, desktop com necessidades diferentes
- Dados relacionados: Entidades com muitos relacionamentos
- Desenvolvimento ágil: Schema evolui rapidamente
- Over-fetching é problema: Largura de banda é limitada (mobile)
- Developer experience: Times grandes se beneficiam da tipagem forte
Padrões Avançados: O Melhor dos Dois Mundos
Em 2025, muitas empresas usam uma abordagem híbrida. Veja um exemplo de como combinar REST e GraphQL:
// Híbrido: REST para operações simples, GraphQL para queries complexas
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// REST endpoints para operações simples e públicas
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: Date.now() });
});
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
// Webhook processing (REST é melhor aqui)
const signature = req.headers['stripe-signature'];
// Processar evento do Stripe
res.sendStatus(200);
});
// File upload (REST é mais simples)
app.post('/api/upload', (req, res) => {
// Upload de arquivo
res.json({ url: 'https://cdn.example.com/file.jpg' });
});
// GraphQL para queries complexas e relacionadas
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
// Performance: DataLoader para batching e caching
context: async () => ({
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI()
}
})
});
await apolloServer.start();
// Mount GraphQL endpoint
app.use('/graphql', expressMiddleware(apolloServer));
// Rate limiting diferenciado
import rateLimit from 'express-rate-limit';
const restLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100 // 100 requests
});
const graphqlLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50, // Queries GraphQL podem ser mais pesadas
keyGenerator: (req) => {
// Rate limit baseado em complexidade da query
return `${req.ip}-${req.body.query ? 'complex' : 'simple'}`;
}
});
app.use('/api/*', restLimiter);
app.use('/graphql', graphqlLimiter);
app.listen(4000, () => {
console.log('Hybrid API server running on port 4000');
console.log('REST endpoints: /api/*');
console.log('GraphQL endpoint: /graphql');
});
DataLoader para Evitar N+1 Queries
Um dos problemas comuns em GraphQL é o problema N+1. DataLoader resolve isso:
import DataLoader from 'dataloader';
// DataLoader para batching automático de queries
class UserAPI {
constructor() {
this.loader = new DataLoader(async (userIds) => {
console.log(`Batch loading users: ${userIds.join(', ')}`);
// Uma única query para múltiplos IDs
const users = await db.users.findMany({
where: { id: { in: userIds } }
});
// Retornar na mesma ordem dos IDs
return userIds.map(id =>
users.find(user => user.id === id)
);
});
}
async getUser(id) {
return this.loader.load(id);
}
async getUsers(ids) {
return this.loader.loadMany(ids);
}
}
// Usar no resolver
const resolvers = {
Post: {
author: async (parent, _, { dataSources }) => {
// Múltiplas chamadas serão agrupadas automaticamente!
return dataSources.userAPI.getUser(parent.authorId);
}
},
Comment: {
author: async (parent, _, { dataSources }) => {
// Mesmo se houver 100 comments, apenas 1 query ao DB
return dataSources.userAPI.getUser(parent.authorId);
}
}
};
// Com DataLoader:
// 10 posts com 50 comments = 1-2 queries ao DB
// Sem DataLoader:
// 10 posts com 50 comments = 60+ queries ao DB!
Performance e Monitoramento
Tanto REST quanto GraphQL precisam de monitoramento adequado:
// Middleware de performance para REST
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`
});
// Enviar métricas para serviço de monitoramento
metrics.record('api.request', {
endpoint: req.path,
duration,
status: res.statusCode
});
});
next();
});
// Plugin Apollo para monitorar GraphQL
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
// Performance monitoring
{
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse({ request, response }) {
const duration = Date.now() - start;
console.log({
operation: request.operationName,
query: request.query,
duration: `${duration}ms`
});
// Alertar se query é muito lenta
if (duration > 1000) {
console.warn(`Slow query detected: ${request.operationName}`);
}
}
};
}
},
ApolloServerPluginLandingPageLocalDefault({ embed: true })
]
});
O Futuro das APIs em 2025 e Além
A evolução não para. As tendências mais empolgantes incluem:
- GraphQL Federation: Microservices com schema unificado
- REST com JSON:API: Padronização de respostas REST
- tRPC: Type-safe APIs sem schema explícito
- gRPC-Web: Performance de gRPC no navegador
- Server-Driven UI: APIs que retornam componentes, não apenas dados
O consenso em 2025? Não há vencedor absoluto. As melhores arquiteturas combinam diferentes abordagens baseadas no caso de uso específico. GraphQL brilha em aplicações complexas com múltiplos clientes, enquanto REST continua imbatível para APIs simples e públicas.
O importante é entender os trade-offs e escolher a ferramenta certa para o problema certo. Como desenvolvedor moderno, dominar ambas as abordagens te torna muito mais valioso no mercado.
Se você quer se aprofundar mais em arquitetura de APIs, recomendo ler meu artigo sobre Microservices com Node.js: Arquitetura Moderna em 2025 onde exploro padrões de comunicação entre serviços.
Bora pra cima! 🦅
💻 Domine JavaScript de Verdade
O conhecimento que você adquiriu neste artigo é só o começo. Há técnicas, padrões e práticas que transformam desenvolvedores iniciantes em profissionais requisitados.
Invista no Seu Futuro
Preparei um material completo para você dominar JavaScript:
Formas de pagamento:
- 3x de R$34,54 sem juros
- ou R$97,90 à vista