Skip to main content

GraphQL

Entendiendo GraphQL de forma sencilla

GraphQL es una forma moderna de trabajar con APIs que te permite pedir exactamente los datos que necesitas, ni más ni menos. A diferencia del estilo tradicional REST, con GraphQL puedes hacer consultas a medida y obtener todo lo necesario en una sola llamada.

¿Qué hace a GraphQL tan especial?

Imagina que vas a una tienda y en lugar de aceptar solo combos prediseñados, puedes elegir exactamente lo que quieres poner en tu carrito. Así funciona GraphQL. En lugar de depender de múltiples rutas fijas como en REST (una para cada cosa), haces una sola consulta donde indicas todo lo que quieres. Eso hace que tu app sea más rápida y que el código sea más limpio.

Esto soluciona dos problemas comunes:

  • Over-fetching: cuando una API REST devuelve más datos de los que realmente necesitas.
  • Under-fetching: cuando tienes que hacer varias llamadas para reunir toda la información.

Con GraphQL haces una sola petición personalizada y obtienes solo lo que pediste.

Esquemas y resolvers

GraphQL no es una base de datos, ni un servidor, ni un lenguaje de programación.

GraphQL es un lenguaje de consultas y un sistema de tipos que permite definir cómo deben pedirse los datos y cómo deben devolverse.

Eso significa que se necesitan dos cosas:

  1. Un mapa que describa los datos. Este mapa son los esquemas. El esquema es la descripción de los tipos, campos, relaciones, y operaciones (consultas y mutaciones).
  2. Un conjunto de funciones que obtengan esos datos de verdad, Estas funciones son los resolvers o resolutores. Los resolvers le dicen a GraphQL: "si el cliente te pide este campo, obtén los datos haciendo esto".

GraphQL actúa como un intermediario muy organizado entre el cliente y tus funciones reales de obtención de datos.

Para entenderlo mejor, te lo explico con un proyecto real. Empezamos con la estructura de archivos y directorios.

Estructura del proyecto

Aquí tienes una estructura de archivos simple y real:

graphql-demo/

├─ src/
│ ├─ index.js ← servidor principal Express + GraphQL
│ ├─ schema/
│ │ ├─ typeDefs.js ← tipos y operaciones GraphQL
│ │ └─ resolvers.js ← funciones que responden a las consultas
│ ├─ data/
│ │ ├─ users.js ← datos falsos
│ │ └─ posts.js ← datos falsos
│ └─ db/
│ └─ operations.js ← lógica de acceso a datos (simulada)

└─ package.json

Datos falsos

src/data/users.js

// Simulamos usuarios
// En un proyecto real esto sería SQL, Mongo, JSON, etc.
export const users = [
{ id: "1", name: "Ana", age: 30 },
{ id: "2", name: "Luis", age: 25 }
];

src/data/posts.js

// Simulamos posts
// Cada post tiene un userId que relaciona el post con un usuario
export const posts = [
{ id: "101", title: "Primer post", userId: "1" },
{ id: "102", title: "Segundo post", userId: "1" },
{ id: "103", title: "Introducción a GraphQL", userId: "2" }
];

Operaciones de “base de datos”

src/db/operations.js

// Este archivo centraliza cómo accedemos a los datos.
// La API GraphQL NO conoce cómo se guardan los datos, solo los pide aquí.

import { users } from "../data/users.js";
import { posts } from "../data/posts.js";

export const db = {
// Obtiene todos los usuarios
getUsers() {
return users;
},

// Busca un usuario por ID
getUserById(id) {
return users.find(u => u.id === id);
},

// Obtiene los posts de un usuario concreto
getPostsByUser(id) {
return posts.filter(p => p.userId === id);
},

// Crea un usuario nuevo
createUser(name, age) {
const newUser = {
id: String(Date.now()),
name,
age
};

users.push(newUser);
return newUser;
}
};

Definición del Schema GraphQL

Un esquema es la definición formal de tu API.. Es un archivo (o conjunto de archivos) donde declaras:

  • Qué tipos de datos existen
  • Qué campos tiene cada tipo
  • Qué tipo de operaciones pueden hacerse
  • Cómo están relacionados esos tipos entre sí

En otras palabras, el esquema es el contrato. Es la parte estática, la parte que describe la forma del mundo.

Ejemplo conceptual

Si tu aplicación tiene usuarios y posts, el esquema describe:

  • El tipo User y sus campos
  • El tipo Post y sus campos
  • Qué consultas se pueden hacer
  • Qué mutaciones se pueden hacer
  • Qué relaciones existen (por ejemplo: un usuario tiene posts)

El esquema no obtiene datos, no consulta la base de datos, no ejecuta lógica. Solo declara la estructura que tendrá la API GraphQL.

Por qué se necesita un esquema

El esquema cumple varias funciones fundamentales:

  1. Permite que el cliente sepa qué datos puede pedir
  2. Permite que GraphQL valide una consulta antes de ejecutarla
  3. Sirve para que herramientas como GraphiQL o Playground generen documentación automáticamente
  4. Hace predecible y consistente la API
  5. Permite que los resolvers sepan qué tipos deben manejar

Sin un esquema, el servidor no sabría interpretar ningún query.

src/schema/typeDefs.js

// Aquí definimos los tipos GraphQL y las operaciones permitidas.
// Esto NO obtiene datos — solo describe la forma de los datos.

import { gql } from "apollo-server-express";

export const typeDefs = gql`

# Tipo User: describe la forma de un usuario
type User {
id: ID!
name: String!
age: Int!
posts: [Post] # Relación: un usuario tiene varios posts
}

# Tipo Post
type Post {
id: ID!
title: String!
user: User # Relación inversa: un post pertenece a un usuario
}

# Consultas disponibles (equivalente a GET)
type Query {
users: [User] # Obtener todos los usuarios
user(id: ID!): User # Obtener un usuario por id
}

# Mutaciones disponibles (equivalente a POST/PUT/DELETE)
type Mutation {
createUser(name: String!, age: Int!): User
}
`;

Resolvers

Los resolvers son funciones que realmente obtienen o calculan los datos. Mientras que el esquema dice “existe un campo llamado name”, el resolver dice “para obtener el valor de name, ejecuta esta función”.

Un resolver:

  • Es una función JavaScript (en el caso de Node.js)
  • Recibe argumentos y contexto
  • Devuelve datos reales

Es la parte dinámica de GraphQL.

Por qué se necesitan resolvers

El esquema define la forma, pero no sabe de dónde salen los datos.

Los resolvers:

  • Acceden a bases de datos
  • Llaman a APIs externas
  • Transforman datos
  • Valida permisos
  • Realizan lógica de negocio

Cada campo del esquema puede tener su propio resolver. Si no defines un resolver, GraphQL usa un resolver por defecto que simplemente devuelve la propiedad del objeto con el mismo nombre, si existe.

src/schema/resolvers.js

// Los resolvers son funciones que devuelven datos reales.
// Cada campo del schema tiene asociado un resolver.

import { db } from "../db/operations.js";

export const resolvers = {
Query: {
// Resolver para "users"
// Cuando el cliente pide `users`, esta función se ejecuta
users: () => {
return db.getUsers();
},

// Resolver para "user(id: ...)"
user: (_, args) => {
// args contiene los parámetros enviados desde la consulta GraphQL
// Ejemplo: { id: "1" }
return db.getUserById(args.id);
}
},

Mutation: {
// Resolver para crear un usuario nuevo
createUser: (_, args) => {
return db.createUser(args.name, args.age);
}
},

// Resolvers anidados: modelos relacionados
User: {
// Cuando se pide users { posts { ... } }
// GraphQL ejecuta esta función automáticamente
posts: (parent) => {
// parent es el usuario actual.
return db.getPostsByUser(parent.id);
}
},

Post: {
user: (parent) => {
return db.getUserById(parent.userId);
}
}
};

Servidor principal Express + GraphQL

src/index.js

// Este archivo levanta un servidor Express con soporte para GraphQL.
// Usa Apollo Server para manejar el esquema y los resolvers.

import express from "express";
import { ApolloServer } from "apollo-server-express";
import { typeDefs } from "./schema/typeDefs.js";
import { resolvers } from "./schema/resolvers.js";

async function startServer() {
const app = express();

// Creamos el servidor GraphQL
const server = new ApolloServer({
typeDefs,
resolvers
});

await server.start();
server.applyMiddleware({ app, path: "/graphql" });

const PORT = 4000;

app.listen(PORT, () => {
console.log("Servidor GraphQL disponible en http://localhost:" + PORT + "/graphql");
});
}

startServer();

PRUEBAS REALES

Cuando estés en:

http://localhost:4000/graphql

Puedes enviar esta consulta:

1. Obtener todos los usuarios

{
users {
id
name
age
posts {
id
title
}
}
}

El servidor resolverá:

  • Resolver de Query.users
  • Resolver de User.posts para cada usuario

Y te devolverá exactamente lo que pediste.

2. Obtener un usuario por ID

{
user(id: "1") {
name
posts {
title
}
}
}

Solo te dará lo que pediste.

3. Crear un usuario nuevo

mutation {
createUser(name: "Carlos", age: 40) {
id
name
age
}
}

Este ejecuta el resolver de Mutation.createUser.

Resumen final

GraphQL se basa en:

  1. Un schema que define qué existe y qué se puede pedir.
  2. Un conjunto de resolvers que explican cómo obtener esos datos.
  3. Un único endpoint para todo.
  4. Puedes pedir exactamente lo que quieras, sin rutas adicionales.

¿GraphQL es mejor que REST?

No se trata de que uno sea “mejor” que el otro, sino de elegir el adecuado según lo que necesites.

REST

  • Usa varias URLs como /users, /users/1.
  • Usa métodos HTTP (GET, POST, PUT, DELETE).
  • Es fácil de cachear con herramientas estándar del navegador.

GraphQL

  • Tiene solo un endpoint (/graphql).
  • Tú defines la forma y profundidad de los datos que quieres.
  • Evita datos innecesarios y múltiples llamadas.
  • Es más flexible, pero requiere definir un esquema y resolvers.
  • La caché es más compleja, aunque existen soluciones como Apollo Client.

¿Vale la pena usar GraphQL?

Si tu aplicación necesita obtener datos de forma flexible, evitar múltiples peticiones o trabajar con conexiones móviles donde el rendimiento importa, . GraphQL puede darte el control que REST no siempre ofrece. Eso sí, aprender a definir esquemas y configurar resolvers lleva algo de tiempo, pero es una inversión que vale la pena.

Reflexión final

GraphQL cambia la forma en que diseñamos APIs. Al poner al cliente en control de los datos que recibe, mejora la eficiencia y la experiencia del usuario. Pero como todo en desarrollo, hay que elegir la herramienta adecuada para el problema que tienes.

¿Y tú? Te animas a probar GraphQL en tu próximo proyecto o migrar parte de tu backend para ganar más flexibilidad?

Para terminar, mira como se implementa en la realidad estas dos metodologías, de un lado REST de otro GraphQL. Mismo modelo de datos, misma lógica, pero dos formas distintas de pedir información.

Estructura general de carpetas

Imagina este proyecto:

rest-vs-graphql/

├─ common/
│ ├─ data/
│ │ ├─ users.js
│ │ └─ posts.js
│ └─ db/
│ └─ operations.js

├─ rest-api/
│ └─ server.js

└─ graphql-api/
├─ schema.js
├─ resolvers.js
└─ server.js

Los dos servidores (REST y GraphQL) usan los mismos datos y “operaciones de DB”, para que la comparación sea justa.

Módulos comunes (datos y “DB” falsa)

common/data/users.js

// common/data/users.js
// Datos de ejemplo: lista de usuarios en memoria.
// En un proyecto real, esto vendría de una base de datos (MySQL, Mongo, etc.).

export const users = [
{ id: "1", name: "Ana", age: 30 },
{ id: "2", name: "Luis", age: 25 },
{ id: "3", name: "Marta", age: 28 }
];

common/data/posts.js

// common/data/posts.js
// Lista de posts. Cada post pertenece a un usuario (userId).

export const posts = [
{ id: "101", title: "Primer post de Ana", userId: "1" },
{ id: "102", title: "Segundo post de Ana", userId: "1" },
{ id: "103", title: "Post de Luis sobre Node", userId: "2" },
{ id: "104", title: "Post de Marta sobre CSS", userId: "3" }
];

common/db/operations.js

// common/db/operations.js
// Este módulo simula la capa de acceso a datos.
// La “API REST” y la “API GraphQL” llamarán a estas funciones,
// de forma que la lógica de datos está centralizada aquí.

import { users } from "../data/users.js";
import { posts } from "../data/posts.js";

export const db = {
// Devuelve todos los usuarios
getUsers() {
return users;
},

// Devuelve un usuario por id o undefined si no existe
getUserById(id) {
return users.find((u) => u.id === id);
},

// Devuelve todos los posts
getPosts() {
return posts;
},

// Devuelve los posts de un usuario concreto
getPostsByUserId(userId) {
return posts.filter((p) => p.userId === userId);
},

// Crea un usuario nuevo en memoria y lo devuelve
createUser(name, age) {
const newUser = {
id: String(Date.now()), // id simple basado en timestamp (solo para demo)
name,
age
};
users.push(newUser);
return newUser;
}
};

Ejemplo REST completo

Ahora una API REST típica con varias rutas:

  • GET /users → lista de usuarios
  • GET /users/:id → un usuario concreto
  • GET /users/:id/posts → posts de un usuario
  • POST /users → crear usuario

rest-api/server.js

// rest-api/server.js
// Servidor HTTP básico con Node.js puro que expone una API REST.
// No usamos Express ni ningún otro framework, solo módulos nativos.

import http from "http";
import { parse } from "url";
import { db } from "../common/db/operations.js";

// Función auxiliar para leer el body de las peticiones POST/PUT como JSON
function readRequestBody(req) {
return new Promise((resolve, reject) => {
let body = "";

// Se lanza cada vez que llegan datos en el cuerpo
req.on("data", (chunk) => {
body += chunk.toString();
});

// Cuando termina de llegar el cuerpo
req.on("end", () => {
if (!body) {
// Si no hay cuerpo, devolvemos un objeto vacío
return resolve({});
}
try {
const json = JSON.parse(body);
resolve(json);
} catch (error) {
// Si el JSON está mal formado, rechazamos la promesa
reject(new Error("JSON inválido en el cuerpo de la petición"));
}
});

req.on("error", (err) => {
reject(err);
});
});
}

// Función auxiliar para responder en JSON
function sendJson(res, statusCode, data) {
const json = JSON.stringify(data);
res.writeHead(statusCode, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(json)
});
res.end(json);
}

// Creamos el servidor HTTP
const server = http.createServer(async (req, res) => {
// parseamos la URL para obtener pathname y query
const parsedUrl = parse(req.url, true);
const method = req.method;
const pathname = parsedUrl.pathname;

// Ejemplo rutas REST:
// GET /users
// GET /users/:id
// GET /users/:id/posts
// POST /users

// Para simplificar, separamos la ruta por "/"
const segments = pathname.split("/").filter(Boolean); // quita elementos vacíos

// CORS muy simple para poder probar con fetch desde frontend si quieres
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");

if (method === "OPTIONS") {
// Respuesta rápida para preflight
res.writeHead(204);
return res.end();
}

// Ruta: GET /users
if (method === "GET" && segments.length === 1 && segments[0] === "users") {
const allUsers = db.getUsers();
return sendJson(res, 200, allUsers);
}

// Ruta: GET /users/:id
if (method === "GET" && segments.length === 2 && segments[0] === "users") {
const userId = segments[1];

const user = db.getUserById(userId);
if (!user) {
return sendJson(res, 404, { error: "Usuario no encontrado" });
}
return sendJson(res, 200, user);
}

// Ruta: GET /users/:id/posts
if (
method === "GET" &&
segments.length === 3 &&
segments[0] === "users" &&
segments[2] === "posts"
) {
const userId = segments[1];
const user = db.getUserById(userId);
if (!user) {
return sendJson(res, 404, { error: "Usuario no encontrado" });
}
const userPosts = db.getPostsByUserId(userId);
return sendJson(res, 200, userPosts);
}

// Ruta: POST /users → crear un usuario nuevo
if (method === "POST" && segments.length === 1 && segments[0] === "users") {
try {
const body = await readRequestBody(req);
const { name, age } = body;

// Validaciones muy básicas
if (!name || typeof name !== "string") {
return sendJson(res, 400, { error: "Campo 'name' obligatorio" });
}
if (typeof age !== "number") {
return sendJson(res, 400, { error: "Campo 'age' obligatorio y debe ser número" });
}

const newUser = db.createUser(name, age);
return sendJson(res, 201, newUser);
} catch (error) {
return sendJson(res, 400, { error: error.message });
}
}

// Si ninguna ruta coincide, devolvemos 404
sendJson(res, 404, { error: "Ruta no encontrada en la API REST" });
});

// Arrancamos servidor REST
const PORT = 3000;
server.listen(PORT, () => {
console.log(`API REST escuchando en http://localhost:${PORT}`);
});

Cómo se usa la API REST

Ejemplos de peticiones:

  1. Obtener usuarios:
curl http://localhost:3000/users
  1. Obtener un usuario concreto:
curl http://localhost:3000/users/1
  1. Obtener posts de un usuario:
curl http://localhost:3000/users/1/posts
  1. Crear usuario:
curl -X POST http://localhost:3000/users ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Carlos\",\"age\":40}"

(En PowerShell, cuidado con las comillas; lo puedes adaptar según tu entorno.)

Fíjate que para obtener “usuarios + sus posts” REST estándar te obliga a:

  • Hacer varias rutas distintas
  • Diseñar endpoints específicos
  • Combinar datos en el cliente

Ejemplo GraphQL minimal

Ahora haremos un servidor que escucha en un único endpoint:

  • POST /graphql

Y recibe un cuerpo JSON con algo así:

{
"query": "{ users { id name } }"
}

Para evitar montar todo el ecosistema de GraphQL real (paquete graphql, etc.) y seguir con Node puro, vamos a hacer una implementación pedagógica y reducida:

  • Soportará solo queries de este estilo:
    • { users { id name age } }
    • { user(id: "1") { id name age } }
    • { user(id: "1") { name posts { title } } }
  • No será compatible al 100 % con el estándar GraphQL, pero sirve para ver el flujo:
    • schema → resolvers → única ruta → respuesta estructurada según lo pedido.

“Schema” conceptual

Lo definimos de forma conceptual en graphql-api/schema.js, sin usar el lenguaje GraphQL, solo como referencia:

// graphql-api/schema.js
// Este archivo NO implementa un parser GraphQL real.
// Solo documenta la forma de los tipos y qué operaciones soportamos,
// para ayudarte a entender el "esquema mental" que estamos usando.

// Query:
// - users: [User]
// - user(id: ID!): User

// Tipos:
// type User {
// id: ID!
// name: String!
// age: Int!
// posts: [Post]
// }
//
// type Post {
// id: ID!
// title: String!
// }

// En un servidor GraphQL real, aquí tendrías "typeDefs" escritos con sintaxis GraphQL.
// Nosotros solo dejaremos una estructura descriptiva para referencia.

export const schemaDescription = {
Query: {
users: "Devuelve todos los usuarios",
user: "Devuelve un usuario por id"
},
User: {
id: "ID del usuario",
name: "Nombre",
age: "Edad",
posts: "Lista de posts del usuario"
},
Post: {
id: "ID del post",
title: "Título del post"
}
};

Es pura documentación, para que visualices el esquema.

Resolvers

Aquí ponemos funciones que saben devolver datos reales.

A diferencia de REST, aquí las agrupamos por “tipo” y “operación”.

// graphql-api/resolvers.js
// Implementación simplificada de resolvers GraphQL.
// No hay librería de GraphQL, así que los usaremos "a mano" desde server.js.

import { db } from "../common/db/operations.js";

export const resolvers = {
Query: {
// Resolver para "users"
users() {
return db.getUsers();
},

// Resolver para "user(id: ...)"
user(_, args) {
return db.getUserById(args.id);
}
},

// Resolvers de campos para el tipo User
User: {
// El campo posts de User
posts(user) {
// 'user' es el objeto User devuelto por Query.users o Query.user
return db.getPostsByUserId(user.id);
}
}
};

Server GraphQL minimal: graphql-api/server.js

Aquí viene lo interesante:

hacemos un servidor HTTP que:

  • Lee el body JSON
  • Extrae query
  • Hace un parseo muy simple de esa query
  • Llama a los resolvers
  • Construye un objeto de respuesta con solo los campos pedidos
// graphql-api/server.js
// Servidor HTTP Node puro que expone un endpoint "tipo GraphQL".
// Es una implementación didáctica, no un servidor GraphQL real completo.
// Soporta consultas muy concretas del estilo:
// { users { id name age } }
// { user(id: "1") { id name posts { title } } }

import http from "http";
import { parse } from "url";
import { resolvers } from "./resolvers.js";

// Función auxiliar para leer el body como JSON
function readRequestBody(req) {
return new Promise((resolve, reject) => {
let body = "";

req.on("data", (chunk) => {
body += chunk.toString();
});

req.on("end", () => {
if (!body) {
return resolve({});
}
try {
const json = JSON.parse(body);
resolve(json);
} catch (error) {
reject(new Error("JSON inválido en el cuerpo de la petición"));
}
});

req.on("error", (err) => {
reject(err);
});
});
}

function sendJson(res, statusCode, data) {
const json = JSON.stringify(data);
res.writeHead(statusCode, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(json)
});
res.end(json);
}

// Función muy simplificada para extraer:
// - operación principal: "users" o "user"
// - argumentos muy básicos
// - lista de campos solicitados
//
// No es un parser GraphQL completo, solo reconoce algunos casos concretos,
// pero ilustra la idea de "pido estos campos y no otros".
function parseQuery(queryString) {
// Eliminamos espacios y saltos de línea redundantes al principio y final
const q = queryString.trim();

// Ejemplos esperados:
// { users { id name age } }
// { user(id: "1") { id name posts { title } } }

// Quitamos llaves externas { ... }
const withoutOuterBraces = q.replace(/^{/, "").replace(/}$/, "").trim();

// Separamos por primera llave que abre campos
// "users { id name }"
// "user(id: \"1\") { id name posts { title } }"
const [operationPart, fieldsPartRaw] = withoutOuterBraces.split("{");
const operationPartTrimmed = operationPart.trim();

// Obtenemos el nombre de la operación y argumentos
let operationName = "";
let args = {};

if (operationPartTrimmed.startsWith("users")) {
operationName = "users";
} else if (operationPartTrimmed.startsWith("user")) {
operationName = "user";
// Extraemos id entre paréntesis: user(id: "1")
const match = operationPartTrimmed.match(/user\s*\(\s*id\s*:\s*"(.+)"\s*\)/);
if (match) {
args.id = match[1];
}
} else {
throw new Error("Operación no soportada en este ejemplo");
}

// Recuperamos la parte de campos hasta la última llave
// fieldsPartRaw: " id name age }" o " id name posts { title } }"
const fieldsPart = fieldsPartRaw.replace(/}$/, "").trim();

// Procesamos campos top-level (del tipo User o lista de User)
// Soportamos:
// id
// name
// age
// posts { title }
const fieldTokens = fieldsPart.split(/\s+/); // división muy básica por espacios

const fields = [];
const nested = {};

let i = 0;
while (i < fieldTokens.length) {
const token = fieldTokens[i];

if (token === "posts") {
// posts { title }
// saltamos la palabra "posts" y la llave "{"
if (fieldTokens[i + 1] !== "{") {
throw new Error("Se esperaba { después de posts");
}
// recopilamos campos dentro de posts
const nestedFields = [];
let j = i + 2; // empezamos tras "{"
while (j < fieldTokens.length && fieldTokens[j] !== "}") {
nestedFields.push(fieldTokens[j]);
j++;
}
nested.posts = nestedFields;
fields.push("posts");
i = j + 1; // continuamos después de "}"
} else {
// Campo plano: id, name, age
fields.push(token);
i++;
}
}

return { operationName, args, fields, nested };
}

// Construye la respuesta en base a los resolvers y los campos solicitados
function executeQuery(parsedQuery) {
const { operationName, args, fields, nested } = parsedQuery;

// Llamamos al resolver de la operación principal
const rootResolver = resolvers.Query[operationName];
if (!rootResolver) {
throw new Error(`Resolver para operación ${operationName} no definido`);
}

const rootResult = rootResolver(null, args || {});

// helper que selecciona solo los campos solicitados de un usuario
function projectUser(user) {
if (!user) return null;
const result = {};

for (const field of fields) {
if (field === "posts") {
// campo anidado
const postsResolver = resolvers.User.posts;
const userPosts = postsResolver(user);
// proyectamos posts según nested.posts (por ejemplo, ["title"])
result.posts = userPosts.map((p) => {
const postResult = {};
const nestedPostFields = nested.posts || [];
nestedPostFields.forEach((nf) => {
if (nf === "title") {
postResult.title = p.title;
}
if (nf === "id") {
postResult.id = p.id;
}
});
return postResult;
});
} else {
// campos planos: id, name, age
if (field in user) {
result[field] = user[field];
}
}
}

return result;
}

// Si la operación devuelve una lista (users)
if (operationName === "users") {
return rootResult.map(projectUser);
}

// Si devuelve un solo usuario (user)
if (operationName === "user") {
return projectUser(rootResult);
}

throw new Error("Operación no soportada en executeQuery");
}

// Creamos servidor HTTP tipo GraphQL
const server = http.createServer(async (req, res) => {
const url = parse(req.url, true);
const method = req.method;
const pathname = url.pathname;

// CORS simple
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");

if (method === "OPTIONS") {
res.writeHead(204);
return res.end();
}

if (method === "POST" && pathname === "/graphql") {
try {
const body = await readRequestBody(req);
const { query } = body;

if (!query || typeof query !== "string") {
return sendJson(res, 400, { error: "Se requiere campo 'query' (string)" });
}

const parsed = parseQuery(query);
const data = executeQuery(parsed);

// Siguiendo la convención GraphQL: { data: ... }
return sendJson(res, 200, { data });
} catch (error) {
return sendJson(res, 400, { error: error.message });
}
}

// Cualquier otra ruta no está soportada
sendJson(res, 404, { error: "Solo se admite POST /graphql en este servidor" });
});

// Arrancamos servidor GraphQL-like
const PORT = 4000;
server.listen(PORT, () => {
console.log(`API "GraphQL-like" escuchando en http://localhost:${PORT}/graphql`);
});

Cómo hacer consultas “GraphQL-like”

  1. Obtener todos los usuarios con ciertos campos:
curl -X POST http://localhost:4000/graphql ^
-H "Content-Type: application/json" ^
-d "{\"query\":\"{ users { id name } }\"}"

Respuesta de ejemplo:

{
"data": [
{ "id": "1", "name": "Ana" },
{ "id": "2", "name": "Luis" },
{ "id": "3", "name": "Marta" }
]
}

Fíjate:

  • Mismo endpoint (/graphql)
  • Tú decides qué campos quieres (id, name…), no la API.
  1. Obtener un usuario por id, incluyendo posts con solo título:
curl -X POST http://localhost:4000/graphql ^
-H "Content-Type: application/json" ^
-d "{\"query\":\"{ user(id: \\\"1\\\") { name posts { title } } }\"}"

Observa cómo:

  • En la query pides name del usuario
  • Y posts { title } de sus posts
  • El servidor construye dinámicamente esa forma de respuesta.

Diferencias REST vs GraphQL vistas en los ejemplos

Con el ejemplo REST:

  • Necesitas varias rutas específicas (/users, /users/:id, /users/:id/posts)
  • Si mañana quieres “usuarios con sus posts en una sola respuesta”, o:
    • haces una ruta nueva (/users-with-posts), o
    • haces varias peticiones desde el cliente y lo combinas tú.

Con el ejemplo GraphQL minimal:

  • Tienes un único endpoint: /graphql
  • La forma de la respuesta no la decide el servidor, la decide tu consulta
  • Puedes hacer:
{ users { id name age } }

o

{ user(id: "2") { name posts { title } } }

sin cambiar nada en el servidor (mientras el esquema y resolvers lo soporten).

Ahora que ya tienes los dos ejemplos completos (REST y GraphQL minimal), vamos a profundizar en las diferencias reales entre ambas arquitecturas, pero basándonos en código y situaciones concretas.

Esta parte es muy importante porque transforma lo que parecía teoría abstracta en decisiones técnicas prácticas que tomarías como programador.

A continuación continuo con:

  1. Comparación técnica a nivel de código
  2. Comparación a nivel de cliente (cómo las consume un frontend)
  3. Qué pasa cuando escala el proyecto
  4. Ejemplos reales de casos donde REST falla y GraphQL resuelve
  5. Ejemplos donde GraphQL no es una buena solución
  6. Propuesta de ejercicio práctico guiado para que lo implementes tú mismo

Sin usar iconos ni emojis, siguiendo tus preferencias.

Comparación técnica a nivel de código

Voy a partir de los dos servidores que te generé. Empecemos desde cómo se escribe cada uno.

REST necesita más rutas, GraphQL solo una

En REST:

GET /users
GET /users/:id
GET /users/:id/posts
POST /users

Cada funcionalidad implica:

  • Diseñar la ruta
  • Implementarla
  • Manejar parámetros
  • Validar
  • Devolver una forma fija de respuesta

El servidor REST que ya hicimos evidencia este crecimiento de rutas.

En GraphQL:

POST /graphql

Solo un endpoint.

La forma de la respuesta no está fijada por el servidor, sino por la consulta del cliente.

REST retorna siempre la misma forma de datos

La ruta GET /users siempre devuelve exactamente esto:

[
{ "id": "1", "name": "Ana", "age": 30 },
{ "id": "2", "name": "Luis", "age": 25 }
]

Si quieres solo el nombre, no puedes.

Si quieres incluir los posts del usuario, REST no te lo devuelve.

Tienes que:

  • Crear una ruta nueva
  • O hacer varias peticiones
  • O modificar el servidor

En GraphQL:

{
users {
name
}
}

Devuelve solo nombres.

O:

{
users {
name
posts { title }
}
}

Devuelve, además, los posts.

No modificas el servidor.

No haces rutas nuevas.

GraphQL es flexible por diseño.

REST obliga a navegar entre endpoints, GraphQL navega por tipos

REST:

  • Para obtener usuarios → GET /users
  • Para posts del usuario → GET /users/:id/posts
  • Para más información → otras rutas

GraphQL:

  • El esquema declara las relaciones (User → posts)
  • Los resolvers implementan cómo obtener esas relaciones
  • El cliente navega pidiendo los campos que quiere
  • El servidor responde combinando datos según lo que el cliente definió

Esto reduce muchísimo el código repetido y la complejidad del backend.

Comparación a nivel de cliente (cómo lo usaría tu frontend)

Voy a simular un frontend minimal hecho con fetch. Esto es muy educativo.

REST desde el frontend

Obtener usuario y posts (dos peticiones)

async function getUserWithPosts(id) {
// 1. Obtener el usuario
const userRes = await fetch(`http://localhost:3000/users/${id}`);
const user = await userRes.json();

// 2. Obtener sus posts
const postsRes = await fetch(`http://localhost:3000/users/${id}/posts`);
const posts = await postsRes.json();

// 3. Combinar manualmente
return {
...user,
posts
};
}

getUserWithPosts("1").then(console.log);

Problemas:

  • Dos llamadas de red
  • Latencia mayor
  • Repetición de lógica
  • Lo haces para cada usuario, multiplicando peticiones

GraphQL desde el frontend

Una sola petición, tú decides los campos

async function getUserWithPosts(id) {
const query = `
{
user(id: "${id}") {
name
posts { title }
}
}
`;

const res = await fetch("http://localhost:4000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query })
});

const json = await res.json();
return json.data;
}

getUserWithPosts("1").then(console.log);

Ventajas:

  • Una sola llamada
  • Sin rutas adicionales
  • Sin lógica de combinación de datos
  • Independencia del backend para cambiar la forma de la respuesta

¿Qué pasa cuando el proyecto crece?

Vamos a imaginar un proyecto real que incluye:

  • Usuarios
  • Posts
  • Comentarios
  • Likes
  • Perfiles
  • Fotos
  • Seguidores
  • Estadísticas
  • Notificaciones

En REST, para satisfacer las necesidades del frontend móvil, web y panel de administración:

  • Tendrás decenas de rutas
  • Muchos endpoints duplicados con pequeñas variaciones
  • Muchas combinaciones para evitar múltiples peticiones
  • Carga en el servidor por mantener compatibilidad con clientes antiguos

En GraphQL:

  • El esquema crece, pero el número de endpoints es 1
  • Los resolvers se dividen por tipos, no por rutas
  • La evolución del API no rompe clientes viejos
  • El cliente antiguo sigue pidiendo solo lo que necesita
  • El cliente nuevo puede pedir más campos sin obligarte a crear rutas nuevas

GraphQL escala mejor en proyectos con muchos requisitos cambiantes.

Casos reales donde REST falla y GraphQL lo resuelve

Caso 1: Una pantalla necesita muchos recursos

REST obliga a:

  • Hacer de 3 a 8 peticiones
  • Combinar la lógica en el cliente

GraphQL permite:

  • Una sola consulta
  • Incluir campos necesarios incluso a varios niveles

Caso 2: Aplicaciones móviles con conexión lenta

REST = varias llamadas → varios tiempos muertos.

GraphQL = una sola llamada → carga más rápida.

Caso 3: Backend frecuentemente modificado

REST = romper clientes o mantener versiones (v1, v2, v3)

GraphQL = el cliente pide lo que necesita

Mientras no elimines campos del esquema, no rompes nada.

Caso 4: Frontend necesita flexibilidad

REST define una forma fija de respuesta.

GraphQL deja que el frontend decida qué forma quiere.

Casos donde GraphQL no es buena idea

Aunque GraphQL tiene muchas ventajas, no siempre es la mejor solución.

No es buena idea cuando:

  1. La aplicación tiene endpoints muy simples, fijos y estables
  2. El cliente no cambia nunca
  3. El backend necesita caché a nivel de ruta
  4. Tienes un tráfico muy alto y quieres máxima simplicidad
  5. Hay limitaciones muy duras de latencia o CPU
  6. El backend no puede procesar consultas arbitrarias por seguridad

Buen ejemplo para REST:

  • Microservicios internos
  • APIs de lectura muy simple
  • Webhooks
  • Servicios muy especializados

GraphQL es ideal para:

  • Aplicaciones complejas
  • Apps con muchos frontends distintos
  • Paneles de administración
  • Sitios donde el cliente decide qué necesita