GraphQL vs REST: eligiendo el estilo de API correcto
GraphQL no reemplaza a REST: es una alternativa con compromisos distintos que importan según tu cliente, el tamaño del equipo y la forma de tus datos.

El debate GraphQL vs. REST muchas veces genera más ruido que claridad. Los equipos adoptan GraphQL porque está de moda, y después batallan con el caching, la autorización y las queries N+1. Otros lo descartan por completo y construyen endpoints REST con 15 query parameters para evitar el over-fetching. La elección correcta depende de tus restricciones específicas — la diversidad de clientes, las relaciones de datos y la experiencia del equipo.
La diferencia central
REST organiza las APIs alrededor de recursos con formas de respuesta fijas. GraphQL le da a los clientes un lenguaje de consulta para pedir exactamente los datos que necesitan.
// REST — multiple requests, potentially over-fetching
// GET /api/users/123
// GET /api/users/123/orders?limit=5
// GET /api/users/123/reviews?limit=3
// Each endpoint returns its full schema, including fields the client doesn't use
// GraphQL — one request, exact data
const query = `
query UserDashboard($userId: ID!) {
user(id: $userId) {
name
avatar
orders(limit: 5) {
id
total
status
}
reviews(limit: 3) {
rating
comment
}
}
}
`;Con REST, el servidor decide qué datos devolver. Con GraphQL, el cliente decide. Esto traslada la complejidad del cliente al servidor.
Dónde gana REST
REST es más simple para operaciones CRUD directas con recursos bien definidos.
// ✅ REST excels at simple, resource-oriented APIs
// Clear, cacheable, easy to understand
app.get("/api/products/:id", getProduct);
app.post("/api/products", createProduct);
app.patch("/api/products/:id", updateProduct);
app.delete("/api/products/:id", deleteProduct);
// HTTP caching works out of the box
// GET /api/products/123
// Cache-Control: public, max-age=300
// ETag: "abc123"// ❌ GraphQL adds complexity for simple operations
const CREATE_PRODUCT = gql`
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
price
}
}
`;
// Every request is POST, every response is 200
// HTTP caching doesn't work without additional infrastructure
// Error handling is an envelope format, not HTTP status codesVentajas de REST: caching HTTP, tooling más simple, semántica de códigos de estado, curva de aprendizaje más pequeña, compatibilidad con CDN.
Dónde gana GraphQL
GraphQL brilla cuando los clientes tienen necesidades de datos diversas — especialmente apps móviles con restricciones de ancho de banda o dashboards que agregan datos de múltiples dominios.
// ❌ REST — the mobile app needs 5 requests for one screen
const user = await fetch("/api/users/me");
const orders = await fetch("/api/users/me/orders?limit=3");
const notifications = await fetch("/api/notifications?unread=true");
const recommendations = await fetch("/api/recommendations?limit=5");
const stats = await fetch("/api/users/me/stats");
// 5 round trips, over-fetching on each response
// ✅ GraphQL — one request, exact data needed
const DASHBOARD = gql`
query Dashboard {
me {
name
avatar
}
myOrders(limit: 3) {
id
total
status
}
notifications(filter: { unread: true }) {
id
message
}
recommendations(limit: 5) {
id
title
image
}
myStats {
totalOrders
totalSpent
}
}
`;Una solicitud reemplaza a cinco. El cliente obtiene exactamente los campos que necesita, nada más. En redes móviles, esto importa.
El problema N+1 en GraphQL
Las queries flexibles de GraphQL crean un desafío del lado del servidor: resolver campos anidados puede disparar cientos de queries a la base de datos.
// This innocent query triggers 1 + N database calls
// 1 query for users, then N queries for each user's orders
const query = `{
users(limit: 50) {
name
orders {
id
total
}
}
}`;// ✅ DataLoader batches and deduplicates database calls
import DataLoader from "dataloader";
const orderLoader = new DataLoader(async (userIds: string[]) => {
// One query for ALL user orders, not one per user
const orders = await db.orders.findMany({
where: { userId: { in: userIds } },
});
// Group by userId and return in the same order as input
const ordersByUser = new Map<string, Order[]>();
for (const order of orders) {
const list = ordersByUser.get(order.userId) ?? [];
list.push(order);
ordersByUser.set(order.userId, list);
}
return userIds.map((id) => ordersByUser.get(id) ?? []);
});
// In the resolver
const resolvers = {
User: {
orders: (user: User) => orderLoader.load(user.id),
},
};DataLoader es esencial para cualquier servidor GraphQL no trivial. Sin él, el rendimiento se degrada exponencialmente con la profundidad de la query.
Complejidad de la autorización
La autorización en REST es directa — el middleware verifica antes de que se ejecute el handler. La autorización en GraphQL debe ocurrir a nivel de campo, porque distintos campos pueden tener distintas reglas de acceso.
// REST — one auth check per endpoint
app.get("/api/users/:id", requireAuth, requireOwnerOrAdmin, getUser);
// GraphQL — auth at the field level
const resolvers = {
User: {
email: (user, args, context) => {
// Only the user themselves or an admin can see email
if (context.userId !== user.id && !context.isAdmin) return null;
return user.email;
},
salary: (user, args, context) => {
// Only HR or the user themselves
if (context.userId !== user.id && !context.roles.includes("hr")) {
throw new ForbiddenError("Cannot access salary");
}
return user.salary;
},
},
};Esta autorización por campo es más granular pero más difícil de auditar. "¿Quién puede ver qué?" se convierte en una pregunta que requiere rastrear el código de los resolvers en lugar de simplemente revisar el middleware de rutas.
Marco de decisión
| Factor | Elige REST | Elige GraphQL |
|---|---|---|
| Diversidad de clientes | 1-2 clientes con necesidades similares | Múltiples clientes con necesidades de datos distintas |
| Forma de los datos | Plana, orientada a recursos | Profundamente anidada, relacional |
| Necesidades de caching | El caching HTTP es importante | Un caching a medida es aceptable |
| Tamaño del equipo | Equipo pequeño, API simple | Equipo más grande, capa de API dedicada |
| Necesidades de tiempo real | SSE o WebSocket por separado | Subscriptions incorporadas |
| Consumidores de la API | Desarrolladores externos | Clientes internos que controlas |
Puntos clave
- REST es más simple para CRUD orientado a recursos con buen soporte de caching HTTP
- GraphQL reduce el over-fetching cuando los clientes tienen necesidades de datos diversas y anidadas
- DataLoader es obligatorio en cualquier servidor GraphQL para evitar problemas de queries N+1
- GraphQL traslada la complejidad al servidor — la autorización, el caching y el rendimiento se vuelven más difíciles
- No elijas por moda — elige según la diversidad de tus clientes, la forma de tus datos y la experiencia de tu equipo


