GraphQL vs REST en 2025: Por Qué APIs Están Cambiando
Hola HaWkers, la guerra entre GraphQL y REST APIs está lejos de terminar, pero en 2025, el panorama quedó mucho más claro. Empresas como Netflix, Shopify, GitHub y Airbnb migraron partes significativas de sus APIs para GraphQL - y hay razones concretas para esto.
Pero aquí está el twist: REST no está muerto. De hecho, para muchos casos de uso, REST continúa siendo la elección más inteligente. Entonces, ¿cómo saber cuál usar? Y más importante: ¿cómo implementar cada uno de forma eficiente en Node.js?
Vamos a bucear profundo en esta comparación con ejemplos prácticos que puedes usar hoy.
REST: El Clásico Que Todavía Domina
REST (Representational State Transfer) es el estándar establecido hace décadas. Su simplicidad y previsibilidad son sus mayores fuerzas:
// API REST tradicional con Express.js
import express from 'express';
import { body, validationResult } from 'express-validator';
const app = express();
app.use(express.json());
// In-memory database (en producción, usa un 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 los usuarios
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 usuario 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 un usuario
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 - Comentarios de un 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 - Crear nuevo usuario
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 - Actualizar usuario
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 // Mantener el ID original
};
res.json({ data: users[userIndex] });
});
// DELETE /api/users/:id - Eliminar usuario
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}`);
});¿El problema clásico de REST? Over-fetching y under-fetching. Para obtener un usuario con sus posts y comentarios, necesitas hacer múltiples solicitudes:
// Cliente consumiendo la REST API - problema N+1
async function getUserWithPostsAndComments(userId) {
// Request 1: Buscar usuario
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Request 2: Buscar posts del usuario
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const userPosts = await postsResponse.json();
// Requests 3-N: Buscar comentarios 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 solicitudes = ¡Problema de performance!
GraphQL: Flexibilidad y Eficiencia
GraphQL resuelve exactamente ese problema. Con una única query, buscas exactamente los datos que necesitas:
// API GraphQL con 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!
}
`;
// Mismos datos del ejemplo 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 - cómo buscar los datos
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);
// Agregar post al usuario
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);
// Agregar comentario al post
const post = posts.find(p => p.id === parseInt(postId));
if (post) {
post.commentIds.push(newComment.id);
}
return newComment;
}
}
};
// Crear 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}`);Ahora, del lado del cliente, una única query resuelve todo:
// Cliente GraphQL - ¡UNA solicitud!
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;
}
// ¡Solo 1 solicitud - mucho más eficiente!
const user = await getUserWithPostsAndComments('1');
console.log(user);
Cuándo Usar REST vs GraphQL
La elección no es binaria. Aquí tienes una guía práctica:
Usa REST cuando:
- API pública simple: APIs públicas con pocos endpoints (ej: webhooks)
- Caching es crítico: REST se beneficia de HTTP caching (CDNs, browsers)
- Equipo pequeño: Menos complejidad para mantener
- CRUD simple: Operaciones básicas sin relaciones complejas
- Performance previsible: Latencia y throughput consistentes
Usa GraphQL cuando:
- Múltiples clientes: Mobile, web, desktop con necesidades diferentes
- Datos relacionados: Entidades con muchas relaciones
- Desarrollo ágil: Schema evoluciona rápidamente
- Over-fetching es problema: Ancho de banda es limitado (mobile)
- Developer experience: Equipos grandes se benefician de la tipificación fuerte
Patrones Avanzados: Lo Mejor de Ambos Mundos
En 2025, muchas empresas usan un enfoque híbrido. Mira un ejemplo de cómo combinar REST y GraphQL:
// Híbrido: REST para operaciones simples, GraphQL para queries complejas
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 operaciones simples y 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 es mejor aquí)
const signature = req.headers['stripe-signature'];
// Procesar evento de Stripe
res.sendStatus(200);
});
// File upload (REST es más simple)
app.post('/api/upload', (req, res) => {
// Upload de archivo
res.json({ url: 'https://cdn.example.com/file.jpg' });
});
// GraphQL para queries complejas y relacionadas
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
// Performance: DataLoader para batching y 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 pueden ser más pesadas
keyGenerator: (req) => {
// Rate limit basado en complejidad de la 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
Uno de los problemas comunes en GraphQL es el problema N+1. DataLoader resuelve esto:
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(', ')}`);
// Una única query para múltiples IDs
const users = await db.users.findMany({
where: { id: { in: userIds } }
});
// Retornar en el mismo orden de los 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 en el resolver
const resolvers = {
Post: {
author: async (parent, _, { dataSources }) => {
// ¡Múltiples llamadas serán agrupadas automáticamente!
return dataSources.userAPI.getUser(parent.authorId);
}
},
Comment: {
author: async (parent, _, { dataSources }) => {
// Incluso si hay 100 comments, solo 1 query al DB
return dataSources.userAPI.getUser(parent.authorId);
}
}
};
// Con DataLoader:
// 10 posts con 50 comments = 1-2 queries al DB
// Sin DataLoader:
// 10 posts con 50 comments = ¡60+ queries al DB!Performance y Monitoreo
Tanto REST como GraphQL necesitan monitoreo adecuado:
// 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 servicio de monitoreo
metrics.record('api.request', {
endpoint: req.path,
duration,
status: res.statusCode
});
});
next();
});
// Plugin Apollo para monitorear 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 si query es muy lenta
if (duration > 1000) {
console.warn(`Slow query detected: ${request.operationName}`);
}
}
};
}
},
ApolloServerPluginLandingPageLocalDefault({ embed: true })
]
});
El Futuro de las APIs en 2025 y Más Allá
La evolución no para. Las tendencias más emocionantes incluyen:
- GraphQL Federation: Microservices con schema unificado
- REST con JSON:API: Estandarización de respuestas REST
- tRPC: Type-safe APIs sin schema explícito
- gRPC-Web: Performance de gRPC en el navegador
- Server-Driven UI: APIs que retornan componentes, no solo datos
¿El consenso en 2025? No hay vencedor absoluto. Las mejores arquitecturas combinan diferentes enfoques basados en el caso de uso específico. GraphQL brilla en aplicaciones complejas con múltiples clientes, mientras REST continúa imbatible para APIs simples y públicas.
Lo importante es entender los trade-offs y elegir la herramienta correcta para el problema correcto. Como desarrollador moderno, dominar ambos enfoques te hace mucho más valioso en el mercado.
Si quieres profundizarte más en arquitectura de APIs, recomiendo leer mi artículo sobre Microservices con Node.js: Arquitectura Moderna en 2025 donde exploro patrones de comunicación entre servicios.
¡Vamos a por ello! 🦅
Domina JavaScript de Verdad
El conocimiento que adquiriste en este artículo es solo el comienzo. Hay técnicas, patrones y prácticas que transforman desarrolladores principiantes en profesionales solicitados.
Invierte en Tu Futuro
Preparé un material completo para que domines JavaScript:
Formas de pago:
- $9.90 USD (pago único)

