From n00b to ZeroCool / Profesionalización

GraphQL: poder, flexibilidad y abuso (sin reventar production)

Cómo usar GraphQL sin incendiar tu backend: esquema con barandales, N+1, caching, límites, auth por campo y observabilidad para production.

Lo que vale la pena leer aquí

Eran las 6:40 pm. Ya olía a tacos, tu laptop ya pedía jubilación y tú jurabas que “nomás era un query” para la app. Deploy en viernes (porque claro). Diez minutos después: CPU al 95%, el pool de conexiones llorando y el PM preguntando por Slack por qué “si GraphQL es más eficiente”.

Eran las 6:40 pm. Ya olía a tacos, tu laptop ya pedía jubilación y tú jurabas que “nomás era un query” para la app. Deploy en viernes (porque claro). Diez minutos después: CPU al 95%, el pool de conexiones llorando y el PM preguntando por Slack por qué “si GraphQL es más eficiente”.

GraphQL es poder. Y el poder sin barandales termina en abuso. No porque sea “malo”, sino porque te deja pedir exactamente lo que quieras… incluyendo cosas que tu backend no está listo para servir.

Qué te llevas

  • Cómo decidir si GraphQL te conviene o si REST te da paz mental.
  • Cómo diseñar un esquema que no invite al abuso.
  • Cómo evitar el infame N+1 (sin rezarle al ORM).
  • Cómo poner límites reales: complejidad, profundidad, rate limits y persistencia.
  • Cómo cachear y observar GraphQL para que production no sea una caja negra.

Por qué GraphQL se siente mágico… hasta que no

GraphQL nació para que el cliente (web, mobile) deje de mendigar endpoints. En vez de /users/1 + /users/1/orders + /orders/99/items, mandas un solo query y listo.

En la vida real (sobre todo cuando estás sacando la chamba con un equipo chico o rotando gente):

  • App móvil con red irregular (Metro, camión, el “LTE” que es 3G con maquillaje). Menos round-trips ayuda.
  • Backend con historial: tablas legacy, un Redis a medias, un job runner que “a veces” corre, y un deadline que no perdona.

GraphQL puede bajar fricción de producto. Pero también puede:

  • Convertir tu base de datos en buffet libre (“dame todos los users con todos los orders con todos los items…”).
  • Hacer más difícil el caching tradicional (no hay URL estable por recurso).
  • Volverte ciego si no instrumentas resolvers y solo ves “/graphql tardó X”.

Decisión práctica: si adoptas GraphQL, adoptas gobernanza (schema, límites, observabilidad). Sin eso, es como dejar un panel admin sin rate limit: no truena el primer día… pero un día truena.

GraphQL con barandales (workflow que sí aguanta)

1) Decide si GraphQL es el martillo correcto

Úsalo cuando:

  • Tienes múltiples clientes (web + iOS + Android) y cada uno ocupa diferentes shapes de datos.
  • Ya te hartaste del overfetch/underfetch de REST y llevas meses parchando endpoints “custom”.
  • Tu dominio se presta a un grafo real (relaciones entre entidades, navegación natural).

Piénsalo dos veces cuando:

  • Solo tienes un cliente y tu REST está estable.
  • El equipo todavía batalla con contratos; GraphQL no te regala disciplina.
  • Tu dolor principal es la DB (índices, queries lentas, locks). GraphQL sin optimización lo amplifica.

Regla de guerra: GraphQL no es “más rápido”; es “más flexible”. El performance te lo ganas (o lo pierdes) en resolvers, capa de datos y límites.

2) Diseña el esquema para guiar (y también frenar)

Haz explícitas las relaciones caras

Si User.orders pega a una tabla grande, que se note en el API:

  • Pagina por default.
  • Evita orders: [Order!]! sin argumentos, porque alguien lo va a usar mal (o sin querer).

Ejemplo (SDL):

type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  name: String!
  orders(first: Int = 20, after: String): OrderConnection!
}

type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
}

type OrderEdge {
  cursor: String!
  node: Order!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

Decisión con filo: las connections (estilo Relay) no son “moda”; son un contrato para que la app no se dispare en el pie.

No expongas tu modelo interno tal cual

Si tu DB tiene users.status_code y orders.total_cents, el esquema puede (y debe) ser más humano:

  • status: UserStatus!
  • total: Money!

Eso te compra libertad. El día que migres de centavos a decimal (o cambies de proveedor), tu API no debería obligarte a un rollback.

3) Evita N+1 con DataLoader (o batching serio)

El N+1 típico:

  • Pides 50 users.
  • Por cada user, el resolver de orders hace una query.
  • Resultado: 51 queries. En staging “jala”. En production te deja sin pool.

Solución clásica: batching por request.

Ejemplo con Node.js + DataLoader (conceptual):

import DataLoader from "dataloader";

function createLoaders({ db }) {
  const ordersByUserId = new DataLoader(async (userIds) => {
    const rows = await db("orders")
      .whereIn("user_id", userIds)
      .select("id", "user_id", "total_cents");

    const map = new Map();
    for (const id of userIds) map.set(id, []);
    for (const r of rows) map.get(r.user_id).push(r);

    return userIds.map((id) => map.get(id));
  });

  return { ordersByUserId };
}

const resolvers = {
  User: {
    orders: (user, args, ctx) => ctx.loaders.ordersByUserId.load(user.id)
  }
};

Tradeoff real: DataLoader arregla batching, pero si te emocionas con load() en loops gigantes y sin paginar, igual te armas un batch monstruoso. Pagina, mide y pon topes.

4) Limita lo que un query puede hacer (profundidad, complejidad, tamaño)

Si dejas GraphQL “abierto”, un cliente puede:

  • Pedir profundidad absurda (user -> orders -> items -> supplier -> otherOrders -> ...).
  • Pedir campos caros en masa (joins pesados, calls a servicios externos, etc.).

Barandales recomendados:

  • Max depth: corta recursión y queries tipo fractal.
  • Max complexity / cost: asigna costo por campo y falla cuando se pase.
  • Max query size: evita que te manden un pergamino de 200 KB.

Decisión práctica: estos límites no son “anti-dev”. Son el equivalente a ponerle fusibles a la instalación eléctrica. Nadie los aprecia… hasta que salvan la noche.

5) Autorización por campo (no solo en Query)

Bug común: validar permisos en Query.user(id) y asumir que todo lo que cuelga de User está permitido.

Y luego aparece:

  • User.email (privado)
  • User.salary (ni debería existir en el esquema, pero alguien ya lo subió en un pull request “rápido”)

Patrón sano: auth en resolvers sensibles.

Pseudo-ejemplo:

const resolvers = {
  User: {
    email: (user, args, ctx) => {
      if (!ctx.viewer) return null;
      if (ctx.viewer.id !== user.id && !ctx.viewer.roles.includes("ADMIN")) {
        throw new Error("FORBIDDEN");
      }
      return user.email;
    }
  }
};

Sí, es talacha. Pero sale más barato que explicar una fuga de datos. Y si tu producto toca datos personales, esto no es “nice to have”.

6) Caching en GraphQL: no existe el cache mágico

REST cachea bonito por URL. GraphQL no, porque el POST cambia por body.

Opciones que sí jalan:

  • Persisted Queries: el cliente manda un hash/ID y el servidor resuelve contra un query registrado.

    • reduces payload
    • puedes cachear por ID
    • evitas queries arbitrarios en production
  • Cache por resolver (DataLoader + cache por request): evita pegarle mil veces a lo mismo dentro de una request.

  • Cache a nivel de gateway (si usas federación) o cache en tu capa de datos: Redis para lecturas calientes, con invalidación decente.

Advertencia: cachear en GraphQL te obliga a ser claro con qué invalida qué. Si tu negocio cambia precios cada minuto y cacheas agresivo, te compras bugs sutiles y tickets infinitos.

7) Observabilidad: mide resolvers, no solo requests

Si solo ves “/graphql tardó 2.5s”, sigues a ciegas. No sabes si el culpable fue:

  • User.orders
  • Order.items
  • un join sin índice
  • un servicio externo lento

Mínimo viable:

  • Log de operationName + duración.
  • Métricas por resolver (tiempo, errores, hits a DB).
  • Tracing (OpenTelemetry) para ver dónde se muere la request.

Realidad: el primer mes con GraphQL vas a descubrir queries que “nadie sabía” que existían. No fue magia. Fue el cliente inventándolos.

GraphQL: poder, flexibilidad y abuso (sin reventar production) - visual explicativa 1
Visual de apoyo: Qué te llevas

Screenshots sugeridos

  • Vista en GraphiQL/Apollo Sandbox con:
    • operación nombrada (operationName)
    • variables
    • respuesta paginada (connection)
  • Un dashboard (Datadog/Grafana) con:
    • latencia p95 de /graphql
    • top 10 resolvers por tiempo
    • conteo de queries a DB por request
  • Un ejemplo de error por límite de complejidad (mensaje claro para frontend).

Errores comunes (y cómo salir del hoyo)

1) “GraphQL es un endpoint, entonces no versiono nada”

Sí versionas, nomás que distinto:

  • Depreca campos (@deprecated(reason: "...")).
  • Mantén compatibilidad hacia atrás.
  • Migra clientes con fechas reales.

Solución práctica: define política. Campo deprecado vive X semanas, luego se elimina. Sin política, tu schema se vuelve museo.

2) “Ya tengo GraphQL, ya no necesito endpoints específicos”

Hay cosas mejores como endpoints o batch jobs:

  • descargas grandes (CSV)
  • webhooks
  • procesos async (generar reporte, export)

Solución: híbrido sin culpa. GraphQL para lectura y composición; REST/Jobs para tareas pesadas.

3) N+1 disfrazado

Ya metiste DataLoader… pero:

  • tu resolver hace await en loop con lógica extra
  • el batching se rompe porque cada módulo crea su propio loader

Solución: un solo set de loaders por request (en context) y revisa cuántas queries dispara cada operación.

4) Sin límites: un query tumba la app

Se siente lejano hasta que pasa. Y pasa en el peor momento: quincena, campaña, Buen Fin, o cuando marketing prende anuncios y tú ya estabas cerrando la laptop.

Solución: agrega max depth + complexity + rate limit. Si te preocupa romper a frontend, empieza en modo report-only (loggea violaciones), luego haces enforcement.

5) Autorización “por tipo” en vez de “por campo”

Dejas un campo sensible colarse en un objeto aparentemente inocente.

Solución: lista de campos sensibles y tests. Sí, tests. Aunque duela, te evita el incendio.

GraphQL: poder, flexibilidad y abuso (sin reventar production) - visual explicativa 2
Visual de apoyo: Por qué GraphQL se siente mágico… hasta que no

Checklist final (para dormir mejor)

  • El esquema tiene paginación en listas grandes.
  • Los resolvers evitan N+1 (DataLoader/batching) y está medido.
  • Hay límites: profundidad, complejidad y tamaño de query.
  • Autorización por campo en datos sensibles.
  • Persisted queries o al menos allowlist para production (si aplica).
  • Observabilidad por resolver (métricas + tracing).
  • Política de deprecación y limpieza de schema.
  • Plan de rollback (feature flag o fallback) si algo se incendia.

FAQ

1) ¿GraphQL reemplaza REST?

No necesariamente. En equipos pequeños, un híbrido suele ser lo más sano: GraphQL para composición de datos y REST/Jobs para tareas pesadas o streams.

2) ¿Cómo evito que el cliente haga queries abusivos?

Con límites (depth/complexity), paginación obligatoria, persisted queries/allowlist en production y observabilidad para detectar patrones raros.

3) ¿Qué es N+1 en GraphQL y por qué duele tanto?

Es cuando cada resolver dispara queries adicionales por cada elemento. En listas se vuelve multiplicación y mata la DB. Se resuelve con batching (DataLoader) y diseño paginado.

4) ¿GraphQL es más lento que REST?

Puede ser más lento o más rápido. GraphQL te da flexibilidad; el performance depende de tu capa de datos, caché, batching, índices y límites.

5) ¿Cómo versiono un esquema GraphQL sin romper clientes?

Con cambios compatibles y deprecación de campos. Evita cambios breaking; introduce nuevos campos, marca viejos como deprecated y elimina con una política con fechas.

Siguiente episodio

En Backend Underworld se pone bueno cuando GraphQL se junta con microservicios: federation, gateways y el costo real de “unificar” todo.

Spoiler: también hay formas bien creativas de abusar de eso.