Profile Software Services

Cómo optimizar APIs REST y GraphQL para alto rendimiento 

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.

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: 

Comparación API REST con GraphQL

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: 

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: 

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

¿Qué es GraphQL y en qué se diferencia de REST?

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.

¿GraphQL es siempre más rápido que REST?

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.

¿Qué es el problema N+1 en APIs?

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.

¿Cómo funciona la caché en REST y GraphQL?

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.

¿Qué tipo de paginación es mejor para APIs?

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: 

GraphQL tiene más sentido cuando: 

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.

Salir de la versión móvil