Hace unos años, si alguien te preguntaba qué API usar para un proyecto nuevo, la respuesta era casi automática: REST. Hoy la pregunta tiene trampa, porque la respuesta correcta depende de qué quieres evitar, no de qué quieres usar. En este contexto, GraphQL se ha consolidado como una alternativa clave cuando el rendimiento y la eficiencia en el consumo de datos son críticos.
Este artículo no es una comparativa de cuál es “mejor”. Es una guía de los problemas de rendimiento concretos que aparecen en producción (over-fetching, N+1, caché rota, paginación costosa) y cómo resolverlos en cada arquitectura.
Qué vas as ver en esta entrada
El punto de partida: un dashboard bancario
Imagina que estás construyendo un panel para un cliente de banca: necesitas mostrar el nombre de la cuenta, el saldo actual y las últimas cinco transacciones.
Con REST, eso son tres requests HTTP independientes:
GET /accounts/123 GET /accounts/123/transactions GET /accounts/123/summary
Con GraphQL, es una sola query que declara exactamente esos campos y recibe exactamente eso:
query {
account(id: "123") {
name
balance
transactions(last: 5) { amount date description }
}
} El resultado en números habla por sí solo:

Over-fetching: el dato que nadie pidió
REST devuelve recursos completos. Si un endpoint /accounts/123 tiene diez campos y tu cliente necesita tres, los otros siete viajan por la red igualmente: se parsean, se almacenan en memoria y se descartan. A pequeña escala no es un gran problema. A escala, es ruido constante que degrada el rendimiento.
GraphQL resuelve esto por diseño: el cliente declara en la query exactamente qué campos necesita y el servidor responde solo con esos. El coste se paga una vez (diseñar el schema y los resolvers); el beneficio se acumula en cada request.
El caso contrario también existe: el under-fetching, donde REST obliga a lanzar múltiples requests para construir una sola vista.
El problema N+1: cuando un bucle destruye tu base de datos
Este es el error de rendimiento más común y más silencioso en cualquier API que trabaje con relaciones. Y aparece en REST y en GraphQL por igual, aunque por razones distintas.
El escenario clásico: tienes 100 cuentas bancarias y quieres mostrar las últimas 5 transacciones de cada una. El código ingenuo hace esto:
// 1 query para obtener las cuentas
const accounts = await db.query('SELECT * FROM accounts LIMIT 100');
// N queries, una por cada cuenta
for (const account of accounts) {
account.transactions = await db.query(
'SELECT * FROM transactions WHERE account_id = ?', [account.id]
);
} Resultado: 101 queries donde debería haber una. Con 100 usuarios concurrentes, eso se convierte en más de 10.000 queries por segundo contra la base de datos en lugar de 100.
La solución en REST: JOIN lateral
En REST, la solución es una sola query con JOIN que recupera todo en una pasada:
SELECT a.*, t.* FROM accounts a LEFT JOIN LATERAL ( SELECT * FROM transactions WHERE account_id = a.id ORDER BY created_at DESC LIMIT 5 ) t ON true
Una query, mismo resultado, 100 veces menos carga en la base de datos.
La solución en GraphQL: DataLoader
En GraphQL el problema aparece en los resolvers. Por diseño, cada resolver es independiente: el resolver de transactions no sabe que habrá otros 99 resolvers haciendo lo mismo. Sin protección, lanza una query individual por cada cuenta.
DataLoader resuelve esto acumulando las peticiones de todos los resolvers durante el mismo tick del event loop y lanzando una sola query batch al final:
const transactionLoader = new DataLoader(async (accountIds) => {
// UNA sola query para todos los IDs acumulados
const transactions = await db.query(
'SELECT * FROM transactions WHERE account_id = ANY($1)',
[accountIds]
);
return accountIds.map(id => transactions.filter(t => t.account_id === id));
}); El resolver de cada cuenta llama a loader.load(account.id) y DataLoader se encarga del batching de forma transparente.
Caché: donde REST gana por infraestructura
Aquí hay una diferencia real entre los dos enfoques que a menudo se subestima en el diseño inicial del sistema.
REST aprovecha la caché HTTP estándar sin código adicional. El stack de infraestructura web lleva mucho tiempo optimizado para cachear GETs:
- El navegador cachea automáticamente con Cache-Control y ETag
- Un CDN como CloudFlare cachea a nivel geográfico sin configuración especial
- Redis actúa como cache-aside por URL con invalidación explícita por evento
GraphQL usa POST para todo (porque las queries son el body de la petición). Un CDN no puede cachear un POST por definición. El navegador tampoco. Hay que construir lógica de caché propia:
- Persisted Queries: enviar un hash en lugar de la query completa, permitiendo GETs y por tanto CDN
- Caché por resolver: TTL diferente según la volatilidad del dato (balance vs nombre de usuario)
- Apollo Client: caché normalizado en el cliente, con deduplicación por entidad
Paginación: el coste que escala mal
La paginación por offset (LIMIT 20 OFFSET 1000) parece inocente hasta que llega a producción con tablas grandes. El problema es estructural: para devolver la página 50, la base de datos tiene que escanear y descartar las 980 filas anteriores.
-- Página 1: rápido SELECT * FROM transactions ORDER BY created_at DESC LIMIT 20 OFFSET 0; -- ~5ms -- Página 100: lento SELECT * FROM transactions ORDER BY created_at DESC LIMIT 20 OFFSET 1980; -- ~80ms -- Página 1.000: muy lento SELECT * FROM transactions ORDER BY created_at DESC LIMIT 20 OFFSET 19980; -- ~800ms
La alternativa es la paginación por cursor: en lugar de un número de página, el cliente envía el ID del último elemento que vio. La query usa el índice directamente y tiene coste constante en cualquier página:
-- Siempre igual de rápido, independientemente de la página SELECT * FROM transactions WHERE id < $cursor ORDER BY id DESC LIMIT 20; -- ~5ms en página 1 o en página 1.000
GraphQL tiene un estándar para esto llamado Relay Cursor Connections, que define cómo exponer edges, nodes y pageInfo en el schema. Para REST, la implementación es libre pero el principio es idéntico.
Preguntas frecuentes sobre APIs REST y GraphQL
GraphQL es un lenguaje de consultas para APIs que permite al cliente pedir exactamente los datos que necesita en una única request. A diferencia de REST, donde cada endpoint devuelve recursos completos, GraphQL evita el over-fetching y el under-fetching, mejorando la eficiencia en el consumo de datos.
No necesariamente. GraphQL puede ser más eficiente en escenarios donde se necesitan múltiples recursos en una sola vista. Sin embargo, si no se implementa correctamente (por ejemplo, sin DataLoader), puede generar problemas de rendimiento como el N+1. REST, por su parte, puede ser muy rápido si aprovecha bien la caché HTTP.
El problema N+1 ocurre cuando una API realiza una consulta inicial y luego múltiples consultas adicionales por cada resultado. Esto genera una sobrecarga en la base de datos. Es común tanto en REST como en GraphQL y se soluciona mediante técnicas como JOINs o herramientas como DataLoader.
REST aprovecha la caché HTTP de forma nativa (CDN, navegador, headers como Cache-Control o ETag).
GraphQL requiere soluciones adicionales, como persisted queries, caché en cliente (Apollo) o caché por resolver.
La paginación por cursor es más eficiente que la paginación por offset, ya que mantiene un coste constante independientemente del volumen de datos. GraphQL utiliza estándares como Relay Cursor Connections, aunque este enfoque también puede aplicarse en REST.
¿Cuándo usar cada uno?
REST tiene más sentido cuando:
- El API es público y necesita ser cacheado agresivamente por CDN
- El equipo es pequeño y la curva de GraphQL no se justifica
- Los recursos son simples, bien delimitados y cambian poco
GraphQL tiene más sentido cuando:
- El cliente necesita flexibilidad real sobre qué datos pedir
- Hay múltiples clientes (web, móvil, terceros) con necesidades distintas
- El grafo de datos es complejo y las relaciones son profundas
Así que, muy resumido: un GraphQL sin DataLoader es peor que REST. Por otro lado, un REST sin caché, pierde la principal ventaja. Elige bien, pero sobre todo implementa bien para no perder las principales ventajas de cada una de las arquitecturas.
Déjanos tu comentario en nuestras redes sociales y síguenos en nuestro canal de YouTube para mantenerte al día sobre lo último en programación.