REST ha sido durante más de dos décadas el estilo dominante para construir APIs web. GraphQL apareció en 2015 como una respuesta de Facebook a los problemas específicos que tenía con REST al construir su aplicación móvil. Desde entonces ha ganado tracción significativa, especialmente en frontend y en aplicaciones con datos complejos. Pero "¿debería usar GraphQL o REST?" sigue siendo una pregunta que vale la pena responder con cuidado, porque la respuesta depende mucho del contexto.
En esta guía vas a entender las diferencias fundamentales entre ambos, los problemas reales que cada uno resuelve, y cuándo tiene sentido elegir uno u otro.
REST: cómo funciona y cuáles son sus problemas
REST (Representational State Transfer) organiza la API alrededor de recursos identificados por URLs. Cada recurso tiene un conjunto de operaciones que corresponden a los métodos HTTP: GET para leer, POST para crear, PUT/PATCH para actualizar, DELETE para eliminar.
# Endpoints típicos de una API REST para una red social
GET /usuarios/123 → datos del usuario 123
GET /usuarios/123/publicaciones → publicaciones del usuario 123
GET /usuarios/123/seguidores → seguidores del usuario 123
POST /publicaciones → crear una publicación
PATCH /publicaciones/456 → actualizar la publicación 456
DELETE /publicaciones/456 → eliminar la publicación 456
El problema del over-fetching
El endpoint GET /usuarios/123 devuelve todo lo que el servidor sabe sobre el usuario, independientemente de lo que necesite el cliente en ese momento.
// Lo que devuelve GET /usuarios/123:
{
"id": 123,
"nombre": "Ana García",
"email": "ana@mail.com",
"bio": "Desarrolladora full-stack...",
"avatar": "https://cdn.ejemplo.com/avatars/123.jpg",
"ubicacion": "Madrid",
"sitio_web": "https://anagarcia.dev",
"fecha_registro": "2022-03-15T10:30:00Z",
"num_publicaciones": 247,
"num_seguidores": 1834,
"num_seguidos": 412,
"configuracion_privacidad": { ... },
"preferencias_notificaciones": { ... }
}
// Lo que la pantalla de lista de usuarios realmente necesita:
// solo el nombre y el avatar
// Estamos transfiriendo 10 veces más datos de los necesarios
El problema del under-fetching (N+1 requests)
Para mostrar el feed de un usuario necesitas varias peticiones encadenadas, cada una esperando a la anterior.
// Para mostrar el perfil completo de un usuario con sus últimas publicaciones
// y los comentarios de cada una:
// Petición 1: datos del usuario
const usuario = await fetch('/usuarios/123');
// Petición 2: publicaciones del usuario
const publicaciones = await fetch('/usuarios/123/publicaciones?limit=10');
// Petición 3 a 12: comentarios de CADA publicación (N peticiones)
const comentarios = await Promise.all(
publicaciones.map(p => fetch(`/publicaciones/${p.id}/comentarios?limit=3`))
);
// Total: 12 peticiones de red para mostrar una sola pantalla
// En dispositivos móviles con latencia alta, esto es muy lento
El problema del versionado
# Cuando la API cambia y hay clientes que no pueden actualizarse inmediatamente:
GET /v1/usuarios/123 # versión antigua, aún en uso
GET /v2/usuarios/123 # versión nueva con campos adicionales
GET /v3/usuarios/123 # versión más nueva con estructura diferente
# Mantener múltiples versiones en paralelo es costoso.
# REST no tiene una solución elegante para la evolución de la API sin romper clientes.
GraphQL: cómo funciona
GraphQL es un lenguaje de consulta para APIs. En lugar de múltiples endpoints, tienes uno solo (normalmente /graphql) y el cliente describe exactamente qué datos necesita en la consulta. El servidor devuelve exactamente eso, ni más ni menos.
# Todas las peticiones van a un único endpoint:
POST /graphql
# El cuerpo de la petición contiene la consulta GraphQL
# Resolver el problema del over-fetching:
# El cliente pide solo los campos que necesita
query {
usuario(id: 123) {
nombre
avatar
}
}
# Respuesta: exactamente lo pedido, nada más
{
"data": {
"usuario": {
"nombre": "Ana García",
"avatar": "https://cdn.ejemplo.com/avatars/123.jpg"
}
}
}
# Resolver el problema del N+1 con una sola consulta:
# Todo en una petición, el cliente describe la forma exacta de los datos
query ObtenerPerfilCompleto($usuarioId: ID!) {
usuario(id: $usuarioId) {
nombre
bio
avatar
publicaciones(limit: 10) {
id
contenido
fecha
likes
comentarios(limit: 3) {
autor {
nombre
avatar
}
texto
}
}
seguidores(limit: 5) {
nombre
avatar
}
}
}
# Una sola petición. El servidor resuelve todos los datos.
El esquema de GraphQL: el contrato entre cliente y servidor
GraphQL es fuertemente tipado. Toda API de GraphQL define un schema que describe todos los tipos disponibles, sus campos y las operaciones que los clientes pueden hacer. Es el contrato entre el frontend y el backend.
# Schema de GraphQL (SDL - Schema Definition Language)
type Usuario {
id: ID! # ID! = no puede ser nulo
nombre: String!
email: String!
bio: String
avatar: String
publicaciones(limit: Int = 10, offset: Int = 0): [Publicacion!]!
seguidores: [Usuario!]!
numSeguidores: Int!
}
type Publicacion {
id: ID!
contenido: String!
autor: Usuario!
fecha: String!
likes: Int!
comentarios: [Comentario!]!
}
type Comentario {
id: ID!
texto: String!
autor: Usuario!
fecha: String!
}
# Queries: operaciones de lectura
type Query {
usuario(id: ID!): Usuario
publicaciones(limite: Int, categoria: String): [Publicacion!]!
buscar(termino: String!): [Publicacion!]!
}
# Mutations: operaciones de escritura
type Mutation {
crearPublicacion(contenido: String!): Publicacion!
editarPublicacion(id: ID!, contenido: String!): Publicacion!
darLike(publicacionId: ID!): Publicacion!
seguirUsuario(usuarioId: ID!): Usuario!
}
# Subscriptions: datos en tiempo real (opcional)
type Subscription {
nuevaPublicacion(usuarioId: ID!): Publicacion!
nuevoComentario(publicacionId: ID!): Comentario!
}
Implementar una API GraphQL en Node.js
# npm install @apollo/server graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// El schema (tipos y operaciones)
const typeDefs = `
type Usuario {
id: ID!
nombre: String!
email: String!
publicaciones: [Publicacion!]!
}
type Publicacion {
id: ID!
titulo: String!
contenido: String!
autor: Usuario!
}
type Query {
usuario(id: ID!): Usuario
publicaciones: [Publicacion!]!
}
type Mutation {
crearPublicacion(titulo: String!, contenido: String!, autorId: ID!): Publicacion!
}
`;
// Datos de ejemplo (en producción esto vendría de la base de datos)
const usuarios = [
{ id: '1', nombre: 'Ana García', email: 'ana@mail.com' },
{ id: '2', nombre: 'Carlos López', email: 'carlos@mail.com' },
];
const publicaciones = [
{ id: '1', titulo: 'Intro a GraphQL', contenido: 'GraphQL es...', autorId: '1' },
{ id: '2', titulo: 'Node.js tips', contenido: 'Tips útiles...', autorId: '1' },
{ id: '3', titulo: 'CSS moderno', contenido: 'Flexbox y Grid...', autorId: '2' },
];
// Los resolvers: funciones que saben cómo obtener cada campo
const resolvers = {
Query: {
usuario: (_, { id }) =>
usuarios.find(u => u.id === id),
publicaciones: () => publicaciones,
},
Mutation: {
crearPublicacion: (_, { titulo, contenido, autorId }) => {
const nueva = {
id: String(publicaciones.length + 1),
titulo,
contenido,
autorId
};
publicaciones.push(nueva);
return nueva;
}
},
// Resolver para campos que no vienen directamente del objeto padre
Usuario: {
publicaciones: (usuario) =>
publicaciones.filter(p => p.autorId === usuario.id),
},
Publicacion: {
autor: (publicacion) =>
usuarios.find(u => u.id === publicacion.autorId),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Servidor GraphQL en ${url}`);
# Consultas de ejemplo desde el cliente (o desde el playground de Apollo)
# Leer usuario con sus publicaciones
query {
usuario(id: "1") {
nombre
email
publicaciones {
titulo
contenido
}
}
}
# Crear una publicación
mutation {
crearPublicacion(
titulo: "Mi nueva publicación"
contenido: "El contenido aquí..."
autorId: "1"
) {
id
titulo
}
}
Implementar una API GraphQL en Python
# pip install strawberry-graphql fastapi uvicorn
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from typing import Optional
@strawberry.type
class Usuario:
id: str
nombre: str
email: str
@strawberry.type
class Publicacion:
id: str
titulo: str
contenido: str
autor: Usuario
# Base de datos simulada
_usuarios = [
Usuario(id="1", nombre="Ana García", email="ana@mail.com"),
Usuario(id="2", nombre="Carlos López", email="carlos@mail.com"),
]
_publicaciones = [
Publicacion(id="1", titulo="Intro a GraphQL", contenido="GraphQL es...",
autor=_usuarios[0]),
Publicacion(id="2", titulo="Python tips", contenido="Tips útiles...",
autor=_usuarios[0]),
]
@strawberry.type
class Query:
@strawberry.field
def usuario(self, id: str) -> Optional[Usuario]:
return next((u for u in _usuarios if u.id == id), None)
@strawberry.field
def publicaciones(self) -> list[Publicacion]:
return _publicaciones
@strawberry.type
class Mutation:
@strawberry.mutation
def crear_publicacion(self, titulo: str, contenido: str, autor_id: str) -> Publicacion:
autor = next(u for u in _usuarios if u.id == autor_id)
nueva = Publicacion(
id=str(len(_publicaciones) + 1),
titulo=titulo,
contenido=contenido,
autor=autor
)
_publicaciones.append(nueva)
return nueva
schema = strawberry.Schema(query=Query, mutation=Mutation)
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix="/graphql")
# Ejecutar con: uvicorn main:app --reload
# Playground disponible en: http://localhost:8000/graphql
El problema N+1 en GraphQL y cómo resolverlo con DataLoader
GraphQL resuelve el N+1 para el cliente, pero puede crearlo en el servidor si no se tiene cuidado. Cuando el resolver de autor hace una consulta a la base de datos por cada publicación, terminas con N consultas de base de datos para N publicaciones.
// ❌ Problema N+1 en el servidor:
const resolvers = {
Publicacion: {
autor: async (publicacion) => {
// Si hay 100 publicaciones, esto hace 100 consultas a la BD
return await db.usuarios.findById(publicacion.autorId);
}
}
};
// ✅ Solución con DataLoader: agrupa las consultas en lotes
// npm install dataloader
import DataLoader from 'dataloader';
// El batch function recibe TODOS los IDs acumulados y hace UNA sola consulta
const usuarioLoader = new DataLoader(async (usuarioIds) => {
const usuarios = await db.usuarios.findByIds(usuarioIds);
// DataLoader requiere devolver los resultados en el mismo orden que los IDs
return usuarioIds.map(id => usuarios.find(u => u.id === id));
});
const resolvers = {
Publicacion: {
autor: (publicacion) => {
// DataLoader agrupa todas las llamadas del mismo tick en una sola consulta
return usuarioLoader.load(publicacion.autorId);
}
}
};
// Con 100 publicaciones de 5 autores distintos:
// Sin DataLoader: 100 consultas a la BD
// Con DataLoader: 1 consulta a la BD con los 5 IDs únicos
Diferencias fundamentales: tabla comparativa
| Aspecto | REST | GraphQL |
|---|---|---|
| Endpoints | Múltiples (uno por recurso) | Uno solo (/graphql) |
| Quién decide los datos devueltos | El servidor (campo fijo por endpoint) | El cliente (pide exactamente lo que necesita) |
| Over-fetching | Frecuente (datos innecesarios) | Eliminado por diseño |
| Under-fetching | Frecuente (múltiples peticiones) | Eliminado por diseño |
| Tipado y schema | Opcional (OpenAPI/Swagger) | Obligatorio y central |
| Versionado | Versiones de URL (/v1, /v2) | Evolución sin versiones (deprecar campos) |
| Caché HTTP | Nativo (GET es cacheado por navegadores y CDNs) | Más complejo (todo va por POST, hay que gestionar caché en cliente) |
| Curva de aprendizaje | Baja | Media-alta |
| Herramientas y ecosistema | Maduro y universal | Maduro pero más específico |
| Tiempo real | Webhooks, SSE, WebSockets (separados) | Subscriptions (integrado en el protocolo) |
| Problema N+1 en servidor | No aplica | Necesita DataLoader |
| Idóneo para | APIs públicas, microservicios, sistemas simples | Apps con UI compleja, múltiples clientes, datos muy interconectados |
Cuándo elegir REST
- API pública para terceros: REST es el estándar que todos entienden. Una API pública de GraphQL requiere que los consumidores aprendan GraphQL, lo que puede ser una barrera innecesaria. Stripe, Twilio, GitHub Public API y la mayoría de APIs públicas usan REST.
- Caché es crítica: los GET de REST se cachean de forma nativa en navegadores, CDNs y proxies. GraphQL, al usar POST para las consultas, requiere gestionar el caché de forma explícita (Apollo Client, persisted queries).
- Sistema simple con pocos recursos: si tu API tiene 5-10 endpoints y los clientes son homogéneos, la complejidad de GraphQL no se justifica.
- Equipo sin experiencia en GraphQL: la curva de aprendizaje de GraphQL (schema, resolvers, DataLoader, manejo de errores, autenticación) es real. Un equipo que no la conoce tardará más y cometerá más errores iniciales.
- Microservicios con comunicación entre servicios: para la comunicación interna entre microservicios, REST (o gRPC para alto rendimiento) es más simple y directo.
Cuándo elegir GraphQL
- Múltiples clientes con necesidades distintas: si tienes una app web, una app iOS, una app Android y un cliente de escritorio, cada uno necesita datos distintos de los mismos recursos. Con REST acabas haciendo endpoints específicos por cliente o aceptando over-fetching. GraphQL permite que cada cliente pida exactamente lo que necesita con el mismo backend.
- Datos muy interconectados: si tus datos tienen muchas relaciones (usuarios → publicaciones → comentarios → likes → notificaciones) y necesitas acceder a varios niveles en la misma vista, GraphQL elimina el encadenamiento de peticiones.
- Equipo de frontend que quiere autonomía: con GraphQL, los desarrolladores de frontend pueden añadir campos a sus consultas sin depender de que el backend cree un nuevo endpoint. Esto acelera el desarrollo del producto.
- Evolución frecuente del schema: GraphQL permite añadir campos sin romper clientes existentes y deprecar campos gradualmente, lo que facilita la evolución de la API sin versiones.
- Tiempo real integrado: si necesitas datos en tiempo real (notificaciones, chats, actualizaciones en vivo), las Subscriptions de GraphQL son una solución integrada en el protocolo.
La opción híbrida: GraphQL como capa sobre APIs REST
No es necesario elegir uno u otro de forma excluyente. Un patrón muy habitual es usar GraphQL como una capa de agregación en el frontend sobre APIs REST existentes: el schema de GraphQL expone los datos que el cliente necesita, y los resolvers hacen las llamadas a las APIs REST del backend.
// GraphQL como capa sobre APIs REST existentes
const resolvers = {
Query: {
usuario: async (_, { id }) => {
// El resolver llama a una API REST interna
const res = await fetch(`http://users-service/usuarios/${id}`);
return res.json();
},
pedidos: async (_, { usuarioId }) => {
const res = await fetch(`http://orders-service/pedidos?usuario_id=${usuarioId}`);
return res.json();
}
},
Usuario: {
pedidos: async (usuario) => {
const res = await fetch(`http://orders-service/pedidos?usuario_id=${usuario.id}`);
return res.json();
}
}
};
// El cliente hace una sola consulta GraphQL que el gateway convierte
// en múltiples llamadas a microservicios REST
Este patrón (BFF - Backend for Frontend) permite modernizar la API del cliente sin reescribir los microservicios existentes.
Resumen
- REST organiza la API alrededor de recursos con múltiples endpoints y métodos HTTP. El servidor decide qué datos devuelve. Es simple, universal y bien soportado por CDNs y caché HTTP.
- GraphQL usa un único endpoint. El cliente describe exactamente qué datos necesita. Elimina el over-fetching y el under-fetching, pero añade complejidad en el servidor (schema, resolvers, DataLoader).
- REST es mejor para: APIs públicas para terceros, sistemas simples, cuando la caché HTTP es importante, equipos sin experiencia en GraphQL.
- GraphQL es mejor para: múltiples clientes con necesidades distintas, datos muy interconectados, equipos de frontend que necesitan autonomía, evolución frecuente del schema.
- El problema del N+1 ocurre en el servidor con GraphQL cuando cada resolver hace su propia consulta. DataLoader lo resuelve agrupando las consultas en lotes.
- No son excluyentes: el patrón BFF usa GraphQL como capa de agregación sobre APIs REST existentes, lo que permite adoptar GraphQL de forma gradual sin reescribir el backend.
No hay comentarios todavía. Sé el primero en compartir tu opinión.