Desplegar GraphQL sin una estrategia de caché es un error costoso. Descubrí esto de la peor manera cuando nuestra base de datos alcanzó el 100% de CPU tras migrar desde REST. El culpable no fue el tráfico, fue el infame problema GraphQL N+1 ejecutando miles de consultas redundantes por segundo. Aquí explicamos cómo lo solucionamos utilizando el Patrón DataLoader y cómo protegimos el sistema contra ataques de profundidad.
El Diagnóstico: Por qué GraphQL mata tu DB
En una arquitectura REST tradicional, optimizamos las consultas SQL manualmente. En GraphQL, los resolvers son independientes. Si solicitas una lista de 50 Posts y cada post tiene un Author, el servidor ejecuta:
- 1 consulta para obtener los 50 Posts.
- 50 consultas individuales para obtener el Autor de cada Post.
Esto resulta en 51 peticiones a la base de datos para una sola llamada de API. A escala, esto destruye el Rendimiento Backend.
SELECT * FROM authors WHERE id = ?) repetidas cientos de veces por milisegundo.
Solución: Batching con DataLoader
Para resolver esto, implementamos el Patrón DataLoader. Esta utilidad agrupa (batching) todas las solicitudes de un mismo tick del event loop en una sola consulta a la base de datos.
Aquí está la implementación lista para producción en Node.js/TypeScript:
import DataLoader from 'dataloader';
import { User } from './models'; // Tu ORM (TypeORM, Prisma, Mongoose)
// 1. Definir la función de Batching
// Recibe un array de IDs y DEBE retornar un array de resultados del mismo tamaño y orden.
const batchUsers = async (userIds: readonly string[]) => {
// Ejecuta UNA sola consulta: SELECT * FROM users WHERE id IN (1, 2, 3...)
const users = await User.find({ _id: { $in: userIds } });
// Mapear los resultados al orden original de los IDs (CRUCIAL)
const userMap = new Map(users.map(user => [user.id.toString(), user]));
return userIds.map(id => userMap.get(id) || null);
};
// 2. Crear la instancia (Hacer esto por Request, no globalmente para evitar caché sucio)
export const createLoaders = () => ({
userLoader: new DataLoader(batchUsers),
});
// 3. Uso en el Resolver
const resolvers = {
Post: {
author: (post, args, { loaders }) => {
// En lugar de llamar a la DB directamente, usamos .load()
return loaders.userLoader.load(post.authorId);
},
},
};
Con esta Optimización API, pasamos de 51 consultas a solo 2, independientemente del tamaño de la lista.
Protección: Límites de Complejidad de Consultas
Una vez solucionado el N+1, surge otro problema: clientes maliciosos o ignorantes que envían consultas profundamente anidadas (e.g., author { posts { author { posts ... } } }). Esto puede agotar la memoria del servidor.
Utilizamos graphql-query-complexity para calcular el coste antes de la ejecución.
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
const server = new ApolloServer({
schema,
plugins: [{
requestDidStart: async () => ({
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
// Coste base de 1 por campo
simpleEstimator({ defaultComplexity: 1 }),
],
});
// Umbral máximo permitido
const MAX_COMPLEXITY = 100;
if (complexity > MAX_COMPLEXITY) {
throw new Error(
`Consulta demasiado compleja: ${complexity}. Máximo permitido: ${MAX_COMPLEXITY}`
);
}
console.log(`Complejidad de consulta actual: ${complexity}`);
},
}),
}],
});
Al definir la Complejidad de consultas, establecemos un "presupuesto" de recursos que el cliente puede gastar, protegiendo nuestra infraestructura de denegación de servicio (DoS).
Impacto en Producción (Benchmark)
| Métrica | Sin Optimización | Con DataLoader + Complexity |
|---|---|---|
| DB Queries (Lista de 100 items) | 101 Queries | 2 Queries |
| Latencia Media | 450ms | 45ms |
| Uso de CPU (DB) | 85% - 100% | 15% - 20% |
Conclusión
GraphQL ofrece una flexibilidad increíble en el frontend, pero transfiere la complejidad al backend. No lance a producción sin resolver el problema GraphQL N+1 mediante el Patrón DataLoader y sin establecer límites estrictos de complejidad. Estas dos implementaciones transformaron nuestra API de un punto de fallo constante a un sistema robusto y escalable.
Post a Comment