⏱️ Lectura: 14 min

Pedís un solo campo, “nombre”, a una API REST de usuarios y te llega un JSON de 40 campos que no vas a tocar: ese desperdicio de ancho de banda y de tiempo de parseo se llama over-fetching, y es exactamente el problema que GraphQL nació para eliminar en Facebook, mucho antes de hacerse público en 2015.

📑 En este artículo
  1. TL;DR
  2. Qué es GraphQL y por qué importa
  3. Cómo funciona GraphQL en detalle
  4. Ejemplos prácticos
    1. El “hola mundo”: un schema mínimo
    2. Un schema realista: usuarios y publicaciones
  5. Cómo empezar: montar un servidor GraphQL con Express
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas
  9. Profundizando: consultas persistidas, federación y control de costo
  10. Preguntas frecuentes
    1. ¿GraphQL reemplaza a REST?
    2. ¿GraphQL siempre usa HTTP?
    3. ¿Necesito una base de datos especial para usar GraphQL?
    4. ¿Cómo se versiona un schema de GraphQL?
    5. ¿Qué es el problema N+1 y siempre hay que resolverlo con DataLoader?
    6. ¿GraphQL es más rápido que REST?
  11. Referencias

Con GraphQL el cliente deja de depender de que el backend adivine qué necesita: escribe una consulta que describe la forma exacta de la respuesta, y el servidor la resuelve campo por campo contra sus fuentes de datos. Esta guía explica cómo funciona por dentro, con schema, resolvers y ejecución de consultas, y te deja escribiendo tu propio servidor GraphQL en minutos.

TL;DR

  • GraphQL expone un único endpoint HTTP donde el cliente define la forma exacta de la respuesta con una consulta declarativa.
  • Vas a escribir un schema con type y Query, y levantar un servidor GraphQL funcional en menos de 30 líneas de código.
  • Vas a entender la diferencia entre query, mutation y subscription, y cuándo usar cada una.
  • Vas a identificar el problema N+1 en resolvers anidados y resolverlo con DataLoader.
  • Vas a comparar GraphQL contra REST y gRPC con criterios concretos para decidir cuál conviene en tu proyecto.
  • Vas a usar herramientas de introspección para explorar un schema sin leer documentación externa.
  • Vas a reconocer los riesgos de queries anidadas maliciosas y cómo mitigarlos con límites de profundidad.

Qué es GraphQL y por qué importa

GraphQL es un lenguaje de consultas para APIs y un runtime para ejecutar esas consultas contra tus datos existentes. Lo creó Facebook en 2012 para resolver un problema puntual: la app de iOS necesitaba datos distintos a los de Android y a los de la web, y mantener un endpoint REST específico para cada cliente se volvía inmanejable. Facebook liberó la especificación en 2015, y hoy la mantiene la GraphQL Foundation, con implementaciones oficiales en JavaScript y de terceros en casi todos los lenguajes de servidor.

La diferencia central con REST es quién decide la forma de la respuesta. En REST, el servidor define endpoints fijos (/users/42, /users/42/posts) y cada uno devuelve una estructura predeterminada. En GraphQL hay un único endpoint (normalmente /graphql) y el cliente envía una consulta que declara exactamente qué campos quiere, sin importar cuántos recursos relacionados tenga que atravesar el servidor para resolverlos.

Esto importa por dos razones concretas. Primero, elimina el over-fetching (recibir campos de más) y el under-fetching (tener que hacer varias llamadas encadenadas para armar una sola pantalla). Segundo, desacopla la evolución del cliente de la del servidor: agregar un campo nuevo al schema no rompe a los clientes existentes porque nadie recibe campos que no pidió explícitamente.

El schema también funciona como un contrato fuertemente tipado entre frontend y backend. Herramientas como graphql-code-generator leen el schema y generan automáticamente los tipos de TypeScript para cada consulta, así que un cambio de tipo en el backend rompe la compilación del frontend en vez de fallar recién en producción.

💭 Clave: GraphQL no es una base de datos ni reemplaza a REST por decreto: es una capa de consulta sobre las fuentes de datos que ya tenés, sean bases SQL, microservicios REST o APIs de terceros.
Diagrama de un grafo de datos conectado representando un schema GraphQL
Facebook usó GraphQL internamente desde 2012, antes de liberarlo en 2015. Foto de Arnold Francisca en Unsplash

Cómo funciona GraphQL en detalle

Un servidor GraphQL se arma con dos piezas: un schema, escrito en el Schema Definition Language (SDL), que declara qué tipos de datos existen y qué operaciones se pueden hacer; y un conjunto de resolvers, funciones que saben cómo obtener el valor de cada campo del schema.

Cuando llega una consulta, el servidor la procesa en tres fases: parseo (convierte el texto de la query en un árbol de sintaxis), validación (chequea que cada campo pedido exista en el schema y que los tipos coincidan) y ejecución (recorre el árbol y llama al resolver de cada campo, de afuera hacia adentro).

Cada campo del schema tiene exactamente un resolver. Si un campo no define resolver propio, GraphQL usa uno por defecto que busca una propiedad con el mismo nombre en el objeto padre. Esto es clave para entender el rendimiento: una consulta anidada (usuario, posts, comentarios) dispara un resolver por cada nodo del árbol, no una sola consulta a la base de datos.

El motor de ejecución de graphql-js resuelve los campos de un mismo nivel en paralelo cuando sus resolvers son asíncronos, así que un objeto con diez campos que golpean fuentes distintas no espera a que cada uno termine antes de arrancar el siguiente. La dependencia real aparece entre niveles: para resolver Usuario.publicaciones primero hace falta el id del usuario que devolvió Query.usuario.

flowchart TD
    A["Cliente (web, mobile)"] --> B["Servidor GraphQL"]
    B --> C["Resolver: Query.usuario"]
    C --> D["Resolver: Usuario.posts"]
    D --> E["Resolver: Post.comentarios"]
    B --> F[("Base de datos / APIs internas")]
    C --> F
    D --> F
    E --> F

Ejemplos prácticos

El “hola mundo”: un schema mínimo

Este primer ejemplo levanta el servidor GraphQL más chico posible, con graphql-js puro, sin framework HTTP.

const { graphql, buildSchema } = require('graphql');

const schema = buildSchema(`
  type Query {
    saludo: String
  }
`);

const root = {
  saludo: () => 'Hola desde GraphQL',
};

graphql({ schema, source: '{ saludo }', rootValue: root }).then((respuesta) => {
  console.log(respuesta);
});

Al ejecutarlo, la consola imprime { data: { saludo: ‘Hola desde GraphQL’ } }. La consulta { saludo } le pide al servidor exactamente un campo, y el resolver root.saludo devuelve el string.

Un schema realista: usuarios y publicaciones

Este segundo ejemplo agrega tipos relacionados, típicos de un blog o red social, y una mutation para escribir datos.

const schema = buildSchema(`
  type Usuario {
    id: ID!
    nombre: String!
    publicaciones: [Publicacion!]!
  }

  type Publicacion {
    id: ID!
    titulo: String!
    autor: Usuario!
  }

  type Query {
    usuario(id: ID!): Usuario
  }

  type Mutation {
    crearPublicacion(usuarioId: ID!, titulo: String!): Publicacion
  }
`);

const usuarios = { '1': { id: '1', nombre: 'Marta' } };
const publicaciones = [];

const root = {
  usuario: ({ id }) => ({
    ...usuarios[id],
    publicaciones: publicaciones.filter((p) => p.autorId === id),
  }),
  crearPublicacion: ({ usuarioId, titulo }) => {
    const nueva = { id: String(publicaciones.length + 1), titulo, autorId: usuarioId };
    publicaciones.push(nueva);
    return { ...nueva, autor: usuarios[usuarioId] };
  },
};

Con este schema, un cliente puede pedir usuario(id: “1”) { nombre publicaciones { titulo } } y recibir en una sola ida y vuelta el usuario junto con sus publicaciones, algo que en REST habría requerido dos llamadas (/usuarios/1 y /usuarios/1/publicaciones).

Cómo empezar: montar un servidor GraphQL con Express

Para probar esto en un proyecto real, con endpoint HTTP y una interfaz para explorar el schema, hace falta express y graphql-http.

npm install express graphql-http graphql
const express = require('express');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema } = require('graphql');

const schema = buildSchema(`
  type Query {
    version: String
  }
`);

const app = express();
app.all('/graphql', createHandler({ schema, rootValue: { version: () => '1.0.0' } }));
app.listen(4000, () => console.log('GraphQL en http://localhost:4000/graphql'));

Con el servidor arriba, podés confirmar que responde con una consulta de introspección:

curl -X POST http://localhost:4000/graphql -H 'Content-Type: application/json' -d '{"query": "{ __schema { queryType { name } } }"}'

Si la respuesta trae {“data”:{“__schema”:{“queryType”:{“name”:”Query”}}}}, el servidor está sirviendo el schema correctamente. Esa misma introspección es lo que usan GraphiQL y Apollo Sandbox para autocompletar consultas sin documentación externa.

Casos de uso reales

GitHub reescribió buena parte de su API pública sobre GraphQL: la API v4 de GitHub convive con la REST v3 y permite pedir, en una sola consulta, un repositorio, sus issues y sus pull requests relacionados, algo que en la v3 exigía varias llamadas encadenadas.

Shopify expone su Storefront API en GraphQL para que las tiendas armen exactamente el catálogo que necesitan sin sobrecargar el cliente con campos de inventario interno que no van a mostrar. GitLab también migró buena parte de su API interna a GraphQL por el mismo motivo: un solo cliente, la propia interfaz de GitLab, consumiendo datos muy anidados como proyecto, pipeline, jobs y artifacts.

No todos los casos piden GraphQL. Una API pública simple, con pocos recursos y mucho tráfico de lectura repetida, se beneficia más del caching HTTP nativo de REST (Cache-Control, CDNs que cachean por URL) que de la flexibilidad de consultas de GraphQL, que por diseño rompe ese caching basado en la URL.

Código de un resolver GraphQL en una pantalla
DataLoader agrupa consultas repetidas en un solo lote por ciclo del event loop. Foto de Fotis Fotopoulos en Unsplash

Errores comunes y buenas prácticas

ProblemaPor qué pasaCómo evitarlo
Problema N+1Un resolver anidado dispara una consulta a la base de datos por cada elemento del array padreBatchear con DataLoader: agrupa los ids pedidos en el mismo ciclo y hace una sola consulta con WHERE id IN (…)
Queries anidadas maliciosasUn cliente arma una consulta con anidamiento profundo que multiplica el trabajo del servidorLimitar profundidad máxima y calcular un costo por consulta antes de ejecutarla
Falta de paginaciónUn campo tipo lista sin límites devuelve toda la tabla en una sola respuestaUsar el patrón Relay de conexiones (edges, cursor, pageInfo) desde el diseño del schema
Caching por URL rotoAl usar un único endpoint, el caching HTTP tradicional basado en la URL deja de servirCachear por consulta con persisted queries o con una capa de caching de respuestas

El problema N+1 merece un ejemplo concreto. En el schema de usuarios y publicaciones de más arriba, si el resolver de Usuario.publicaciones hiciera una consulta SQL propia por cada usuario devuelto en una lista, una consulta que pida 50 usuarios con sus publicaciones dispararía 51 consultas a la base: una por el listado, cincuenta por las publicaciones de cada uno.

const DataLoader = require('dataloader');

const publicacionesLoader = new DataLoader(async (usuarioIds) => {
  const filas = await db.query(
    'SELECT * FROM publicaciones WHERE autor_id = ANY($1)',
    [usuarioIds]
  );
  return usuarioIds.map((id) => filas.filter((f) => f.autor_id === id));
});

// en el resolver:
publicaciones: (usuario) => publicacionesLoader.load(usuario.id)

DataLoader agrupa todas las llamadas a .load() que ocurren dentro del mismo ciclo del event loop y las resuelve con una sola consulta batched, así que las 51 consultas del ejemplo anterior se convierten en 2.

⚠️ Ojo: DataLoader cachea por request: si necesitás invalidar un valor dentro de la misma consulta, por ejemplo después de una mutation, hay que llamar explícitamente a loader.clear(id).

Comparativa con alternativas

OpciónCuándo usarlaVentajaLimitación
RESTRecursos simples, CRUD directo, necesitás cachear por URLCaching HTTP nativo, curva de aprendizaje mínimaOver-fetching y under-fetching en datos anidados
GraphQLClientes con necesidades de datos muy distintas (web, mobile) o datos muy anidadosEl cliente pide exactamente lo que necesita, con un solo endpointEl caching HTTP tradicional no aplica, y hay que cuidar el costo de las consultas
gRPCComunicación servicio a servicio de alto rendimiento, con contratos estrictos vía protobufSerialización binaria, streaming bidireccional nativoNo pensado para consumirse directo desde el navegador

Profundizando: consultas persistidas, federación y control de costo

En producción, muchos equipos no envían el texto completo de la query desde el cliente en cada request. En su lugar, registran cada consulta con un hash en un build previo, y el cliente solo manda ese hash. Esto reduce el tamaño de la request y le permite al servidor rechazar cualquier consulta que no esté en la lista blanca, cerrando de raíz el vector de queries maliciosas.

Cuando un solo schema no alcanza porque varios equipos mantienen servicios distintos, entra la federación: cada equipo expone un subgraph con su propio schema y resolvers, y un gateway compone todos los subgraphs en un schema único de cara al cliente.

flowchart TD
    A["Cliente"] --> B["Gateway federado"]
    B --> C["Subgraph: Usuarios"]
    B --> D["Subgraph: Publicaciones"]
    B --> E["Subgraph: Pagos"]
    C --> F[("DB usuarios")]
    D --> G[("DB publicaciones")]
    E --> H[("API de pagos")]

Para datos en tiempo real, como un chat o una notificación, GraphQL define un tercer tipo de operación además de query y mutation. El cliente se subscribe a un evento y el servidor empuja una actualización cada vez que ese evento ocurre, típicamente sobre WebSockets con el protocolo graphql-ws.

sequenceDiagram
    participant C as Cliente
    participant S as Servidor GraphQL
    participant R as Resolvers
    C->>S: envia query GraphQL
    S->>S: parsea el texto a un AST
    S->>S: valida el AST contra el schema
    S->>R: ejecuta resolver por cada campo
    R-->>S: devuelve el valor de cada campo
    S-->>C: responde JSON con la forma pedida

Para confirmar que un límite de profundidad está activo, basta con enviar una consulta con anidamiento mayor al permitido y verificar que el servidor responde con un error de validación antes de tocar los resolvers:

const depthLimit = require('graphql-depth-limit');
const { validate } = require('graphql');

const errores = validate(schema, documentoConsultaProfunda, [depthLimit(5)]);
console.log(errores.length > 0 ? 'Consulta rechazada por profundidad' : 'Consulta aceptada');

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: instalá graphql y graphql-http en un proyecto vacío, copiá el schema de usuarios y publicaciones de este artículo, y agregale un tercer tipo relacionado, por ejemplo Comentario, para practicar cómo se resuelve un nivel más de anidamiento.

Preguntas frecuentes

¿GraphQL reemplaza a REST?

No necesariamente. Muchos equipos los combinan: REST para endpoints simples y cacheables, GraphQL para pantallas que necesitan datos muy anidados o vienen de clientes con requisitos distintos.

¿GraphQL siempre usa HTTP?

La especificación no exige un transporte concreto, pero en la práctica la inmensa mayoría de las implementaciones usan HTTP POST para queries y mutations, y WebSockets para subscriptions.

¿Necesito una base de datos especial para usar GraphQL?

No. GraphQL es una capa de consulta: los resolvers pueden leer de SQL, de otra API REST, de un microservicio o de cualquier fuente que ya tengas.

¿Cómo se versiona un schema de GraphQL?

La práctica habitual es no versionar: se agregan campos y tipos nuevos sin quitar los existentes, y los campos obsoletos se marcan con la directiva @deprecated hasta que ya nadie los consulte.

¿Qué es el problema N+1 y siempre hay que resolverlo con DataLoader?

Es el patrón donde un resolver anidado dispara una consulta por cada elemento de una lista. DataLoader es la solución más común en Node.js, pero cualquier mecanismo de batching y cacheo por request resuelve el mismo problema.

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

No es una garantía automática: reduce el tráfico de red al evitar el over-fetching, pero una consulta mal diseñada, sin límites de profundidad, puede terminar siendo más lenta que varios endpoints REST simples.

Referencias

  • GraphQL.org: especificación oficial, tutoriales y documentación de referencia del lenguaje.
  • graphql-js en GitHub: implementación de referencia en JavaScript mantenida por la GraphQL Foundation.
  • API GraphQL de GitHub: documentación de la API v4 de GitHub, un caso de uso real en producción.
  • Documentación de Apollo: guías sobre Apollo Server, Apollo Federation y patrones de batching como DataLoader.

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de Gabriel Heinzer en Unsplash


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.