🔄 REST API vs GraphQL
📖 Definición
REST API es una arquitectura API tradicional diseñada en torno a recursos utilizando métodos HTTP. GraphQL es un lenguaje de consulta desarrollado por Facebook que proporciona un enfoque API flexible donde los clientes pueden solicitar exactamente los datos que necesitan. REST tiene múltiples endpoints, mientras que GraphQL maneja todos los datos a través de un único endpoint.
🎯 Comprender a través de analogías
Restaurante vs Buffet
REST API = Menú de restaurante
├─ Solo puedes pedir del menú predefinido
├─ Cada menú tiene una composición fija
├─ "Una pizza por favor" → Obtienes la pizza completa
├─ Difícil quitar o agregar ingredientes específicos
└─ Simple y predecible
GraphQL = Buffet
├─ Elige y toma solo lo que quieras
├─ Selecciona solo lo que necesitas
├─ "Solo queso por favor" → Obtienes solo queso
├─ Combina libremente los elementos
└─ Flexible pero puede ser complejo
Préstamo de libros en biblioteca
REST API = Préstamo tradicional
Bibliotecario: "¿Qué libro necesitas?"
Yo: "Libros de informática por favor"
Bibliotecario: "Aquí están todos los libros" (10 libros)
Yo: "Solo necesito el capítulo 1..." (el resto es innecesario)
GraphQL = Préstamo inteligente
Yo: "Solo necesito el capítulo 3 del libro de informática"
Bibliotecario: "Aquí está solo el capítulo 3" (exactamente lo necesario)
Yo: "¡Perfecto!"
⚙️ Cómo funciona
1. Comparación de métodos de solicitud de datos
REST API: Múltiples endpoints
GET /users/1 → Información del usuario
GET /users/1/posts → Publicaciones del usuario
GET /posts/1/comments → Comentarios de la publicación
¡Se necesitan 3 solicitudes en total!
GraphQL: Endpoint único
POST /graphql
{
user(id: 1) {
name
posts {
title
comments {
text
}
}
}
}
¡Todos los datos en 1 solicitud!
2. Over-fetching vs Under-fetching
Problemas de REST API
Over-fetching (recibir datos innecesarios)
GET /users/1
{
"id": 1,
"name": "Juan Pérez",
"email": "juan@example.com",
"phone": "123-456-7890",
"address": "Calle Principal 123...",
"createdAt": "2024-01-01",
// ¡Recibir toda la información cuando solo se necesita el nombre!
}
Under-fetching (datos insuficientes)
GET /users/1 → Información del usuario
GET /users/1/posts → Se necesita solicitud adicional
GET /users/1/friends → Se necesita otra solicitud
// ¡Se requieren múltiples solicitudes!
Solución GraphQL
Solo lo que se necesita
{
user(id: 1) {
name // ¡Solo solicitar el nombre!
}
}
→ { "name": "Juan Pérez" }
Todo de una vez
{
user(id: 1) {
name
posts { title }
friends { name }
}
}
→ ¡Todos los datos en 1 solicitud!
3. Filosofía de diseño de API
REST: Centrado en recursos
┌─────────────────┐
│ /users │ → Lista de usuarios
│ /users/1 │ → Usuario específico
│ /posts │ → Lista de publicaciones
│ /posts/1 │ → Publicación específica
└─────────────────┘
Endpoint para cada recurso
GraphQL: Centrado en consultas
┌─────────────────┐
│ /graphql │ → Todas las solicitudes
└─────────────────┘
↓
┌────────┐
│ Query │ → Leer datos
│Mutation│ → Modificar datos
│Subscribe│ → Suscripción en tiempo real
└────────┘
💡 Ejemplos del mundo real
Ejemplo REST API (Express.js)
// Implementación REST API con Express.js
const express = require('express');
const app = express();
app.use(express.json());
// Datos (se usaría base de datos en la práctica)
const users = [
{
id: 1,
name: 'Juan Pérez',
email: 'juan@example.com',
age: 25
},
{
id: 2,
name: 'María García',
email: 'maria@example.com',
age: 30
}
];
const posts = [
{
id: 1,
userId: 1,
title: 'Introducción a REST API',
content: 'REST es...'
},
{
id: 2,
userId: 1,
title: 'Introducción a GraphQL',
content: 'GraphQL es...'
}
];
// ========== GET: Obtener todos los usuarios ==========
app.get('/api/users', (req, res) => {
res.json(users);
});
// ========== GET: Obtener usuario específico ==========
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
res.json(user);
});
// ========== POST: Crear usuario ==========
app.post('/api/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
age: req.body.age
};
users.push(newUser);
res.status(201).json(newUser);
});
// ========== PUT: Actualizar usuario ==========
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
user.age = req.body.age || user.age;
res.json(user);
});
// ========== DELETE: Eliminar usuario ==========
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
users.splice(index, 1);
res.status(204).send();
});
// ========== GET: Obtener publicaciones del usuario ==========
app.get('/api/users/:id/posts', (req, res) => {
const userId = parseInt(req.params.id);
const userPosts = posts.filter(p => p.userId === userId);
res.json(userPosts);
});
// ========== GET: Obtener publicación específica ==========
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: 'Publicación no encontrada' });
}
res.json(post);
});
app.listen(3000, () => {
console.log('Servidor REST API ejecutándose: http://localhost:3000');
});
Uso del cliente REST API
// ========== Uso de REST API ==========
// 1. Obtener lista de usuarios
const response1 = await fetch('http://localhost:3000/api/users');
const users = await response1.json();
console.log(users);
// 2. Obtener usuario específico
const response2 = await fetch('http://localhost:3000/api/users/1');
const user = await response2.json();
console.log(user);
// 3. Crear usuario
const response3 = await fetch('http://localhost:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Pedro López',
email: 'pedro@example.com',
age: 28
})
});
const newUser = await response3.json();
// 4. Actualizar usuario
const response4 = await fetch('http://localhost:3000/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
age: 26
})
});
// 5. Eliminar usuario
await fetch('http://localhost:3000/api/users/1', {
method: 'DELETE'
});
// ========== Problema: Se necesitan múltiples solicitudes ==========
// ¿Cómo obtener usuario y publicaciones juntos?
const userResponse = await fetch('http://localhost:3000/api/users/1');
const user = await userResponse.json();
const postsResponse = await fetch('http://localhost:3000/api/users/1/posts');
const posts = await postsResponse.json();
console.log({ user, posts }); // ¡2 solicitudes!
Ejemplo GraphQL (Apollo Server)
// Implementación del servidor GraphQL
const { ApolloServer, gql } = require('apollo-server');
// Datos (se usaría base de datos en la práctica)
const users = [
{ id: 1, name: 'Juan Pérez', email: 'juan@example.com', age: 25 },
{ id: 2, name: 'María García', email: 'maria@example.com', age: 30 }
];
const posts = [
{ id: 1, userId: 1, title: 'Introducción a REST API', content: 'REST es...' },
{ id: 2, userId: 1, title: 'Introducción a GraphQL', content: 'GraphQL es...' },
{ id: 3, userId: 2, title: 'Tutorial de Node.js', content: 'Node es...' }
];
// ========== Definición de esquema (Type Definitions) ==========
const typeDefs = gql`
# Tipo Usuario
type User {
id: Int!
name: String!
email: String!
age: Int
posts: [Post!]! # Publicaciones del usuario (relación)
}
# Tipo Publicación
type Post {
id: Int!
title: String!
content: String!
author: User! # Autor de la publicación (relación)
}
# Query (Leer datos)
type Query {
# Todos los usuarios
users: [User!]!
# Usuario específico
user(id: Int!): User
# Todas las publicaciones
posts: [Post!]!
# Publicación específica
post(id: Int!): Post
}
# Mutation (Modificar datos)
type Mutation {
# Crear usuario
createUser(name: String!, email: String!, age: Int): User!
# Actualizar usuario
updateUser(id: Int!, name: String, email: String, age: Int): User
# Eliminar usuario
deleteUser(id: Int!): Boolean!
# Crear publicación
createPost(userId: Int!, title: String!, content: String!): Post!
}
`;
// ========== Resolvers (Cómo obtener datos) ==========
const resolvers = {
Query: {
// Obtener todos los usuarios
users: () => users,
// Obtener usuario específico
user: (parent, args) => {
return users.find(u => u.id === args.id);
},
// Obtener todas las publicaciones
posts: () => posts,
// Obtener publicación específica
post: (parent, args) => {
return posts.find(p => p.id === args.id);
}
},
Mutation: {
// Crear usuario
createUser: (parent, args) => {
const newUser = {
id: users.length + 1,
name: args.name,
email: args.email,
age: args.age
};
users.push(newUser);
return newUser;
},
// Actualizar usuario
updateUser: (parent, args) => {
const user = users.find(u => u.id === args.id);
if (!user) return null;
if (args.name) user.name = args.name;
if (args.email) user.email = args.email;
if (args.age) user.age = args.age;
return user;
},
// Eliminar usuario
deleteUser: (parent, args) => {
const index = users.findIndex(u => u.id === args.id);
if (index === -1) return false;
users.splice(index, 1);
return true;
},
// Crear publicación
createPost: (parent, args) => {
const newPost = {
id: posts.length + 1,
userId: args.userId,
title: args.title,
content: args.content
};
posts.push(newPost);
return newPost;
}
},
// ========== Resolvers de relación ==========
User: {
// Obtener publicaciones del usuario
posts: (parent) => {
return posts.filter(p => p.userId === parent.id);
}
},
Post: {
// Obtener autor de la publicación
author: (parent) => {
return users.find(u => u.id === parent.userId);
}
}
};
// ========== Iniciar servidor ==========
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Servidor GraphQL ejecutándose: ${url}`);
});
Uso del cliente GraphQL
// ========== Ejemplos de consultas GraphQL ==========
// 1. Consultar solo el nombre (Prevenir Over-fetching)
query {
user(id: 1) {
name
}
}
// Respuesta: { "data": { "user": { "name": "Juan Pérez" } } }
// 2. Consultar usuario y publicaciones juntos (Prevenir Under-fetching)
query {
user(id: 1) {
name
email
posts {
title
content
}
}
}
// Respuesta:
{
"data": {
"user": {
"name": "Juan Pérez",
"email": "juan@example.com",
"posts": [
{ "title": "Introducción a REST API", "content": "REST es..." },
{ "title": "Introducción a GraphQL", "content": "GraphQL es..." }
]
}
}
}
// 3. Consultar múltiples recursos simultáneamente
query {
users {
name
}
posts {
title
}
}
// 4. Usar variables
query GetUser($userId: Int!) {
user(id: $userId) {
name
email
}
}
// Variables: { "userId": 1 }
// ========== Mutation (Modificar datos) ==========
// 1. Crear usuario
mutation {
createUser(name: "Pedro López", email: "pedro@example.com", age: 28) {
id
name
email
}
}
// 2. Actualizar usuario
mutation {
updateUser(id: 1, age: 26) {
id
name
age
}
}
// 3. Eliminar usuario
mutation {
deleteUser(id: 1)
}
// ========== Cliente JavaScript ==========
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables
})
});
return response.json();
}
// Consultar usuario y publicaciones
const query = `
query GetUserWithPosts($userId: Int!) {
user(id: $userId) {
name
email
posts {
title
}
}
}
`;
const data = await fetchGraphQL(query, { userId: 1 });
console.log(data);
Integración de React con Apollo Client
// ========== Configuración de Apollo Client ==========
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation, gql } from '@apollo/client';
// Crear cliente
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});
// Aplicar Provider a la aplicación
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}
// ========== Usar consultas ==========
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
title
}
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<ul>
{user.posts.map(post => (
<li key={post.title}>{post.title}</li>
))}
</ul>
</div>
))}
</div>
);
}
// ========== Usar Mutations ==========
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $age: Int) {
createUser(name: $name, email: $email, age: $age) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER);
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
age: parseInt(formData.get('age'))
}
});
};
if (loading) return <p>Creando...</p>;
if (error) return <p>Error: {error.message}</p>;
if (data) return <p>Creado: {data.createUser.name}</p>;
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Nombre" required />
<input name="email" type="email" placeholder="Correo" required />
<input name="age" type="number" placeholder="Edad" />
<button type="submit">Crear usuario</button>
</form>
);
}
// ========== Caché y optimización ==========
const GET_USER = gql`
query GetUser($id: Int!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }) {
const { loading, error, data, refetch } = useQuery(GET_USER, {
variables: { id: userId },
// Política de caché
fetchPolicy: 'cache-first', // Caché primero
// fetchPolicy: 'network-only', // Siempre solicitar del servidor
// fetchPolicy: 'cache-and-network', // Mostrar caché y actualizar
});
// Actualización manual
const handleRefresh = () => {
refetch();
};
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>{data.user.name}</h2>
<p>{data.user.email}</p>
<button onClick={handleRefresh}>Actualizar</button>
</div>
);
}
🤔 Preguntas frecuentes
P1. ¿Debo elegir REST API o GraphQL?
R: Elige según las características de tu proyecto:
✅ Elegir REST API cuando:
├─ Aplicación CRUD simple
├─ Estructura de recursos clara
├─ El caché es importante (utiliza caché HTTP)
├─ El equipo está familiarizado con REST
├─ Mucha carga/descarga de archivos
└─ Ejemplo: Blog, funciones básicas de comercio electrónico
✅ Elegir GraphQL cuando:
├─ Relaciones de datos complejas
├─ Aplicación móvil (ahorro de datos importante)
├─ Clientes diversos (web, app, tablet)
├─ Desea desarrollo frontend rápido
├─ Necesita suscripción de datos en tiempo real
└─ Ejemplo: Redes sociales, tablero, aplicaciones en tiempo real
🤝 También se pueden usar juntos:
├─ API principal usa GraphQL
├─ Carga de archivos usa REST
└─ Aprovechar las ventajas de cada uno
P2. ¿Cuál es la diferencia de rendimiento?
R:
// Características de rendimiento de REST API
Ventajas:
- Puede utilizar caché HTTP
GET /api/users/1
Cache-Control: max-age=3600
- Caché CDN fácil
- Endpoints simples son rápidos
Desventajas:
- Over-fetching transfiere datos innecesarios
GET /api/users/1
→ Devuelve todos los campos (100KB)
- Under-fetching requiere múltiples solicitudes
GET /api/users/1 // 1ª solicitud
GET /api/users/1/posts // 2ª solicitud
GET /api/posts/1/comments // 3ª solicitud
// ¡3 solicitudes en total!
// Características de rendimiento de GraphQL
Ventajas:
- Solo datos exactamente necesarios (10KB)
query {
user(id: 1) {
name
email
}
}
- Todos los datos en una solicitud
query {
user(id: 1) {
name
posts {
title
comments { text }
}
}
}
// ¡Hecho en 1 solicitud!
Desventajas:
- Caché HTTP difícil (usa solicitudes POST)
- Consultas complejas cargan el servidor
- Problema N+1 (se resuelve con DataLoader)
Benchmark real:
REST: 3 solicitudes, 200KB total, 300ms
GraphQL: 1 solicitud, 50KB total, 150ms
→ GraphQL ventajoso en entornos móviles
P3. ¿Qué es el problema N+1 en GraphQL?
R: Se puede resolver con DataLoader:
// ========== Problema N+1 ==========
// Consultar 10 usuarios y sus publicaciones
// Problema: Consultas ineficientes
const resolvers = {
Query: {
users: () => {
return db.users.findAll(); // 1 consulta
}
},
User: {
posts: (user) => {
return db.posts.findByUserId(user.id); // ¡1 consulta por usuario!
}
}
};
// Total de consultas: 1 + 10 = 11
// 1: Obtener usuarios
// 10: Obtener publicaciones de cada usuario
// ========== Resolver con DataLoader ==========
const DataLoader = require('dataloader');
// Obtener publicaciones en lote
const postLoader = new DataLoader(async (userIds) => {
// Obtener todas las publicaciones de una vez
const posts = await db.posts.findByUserIds(userIds);
// Agrupar por usuario
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.userId]) {
postsByUser[post.userId] = [];
}
postsByUser[post.userId].push(post);
});
// Devolver publicaciones de cada usuario
return userIds.map(id => postsByUser[id] || []);
});
const resolvers = {
User: {
posts: (user) => {
return postLoader.load(user.id); // ¡Procesamiento en lote!
}
}
};
// Total de consultas: 1 + 1 = 2
// 1: Obtener usuarios
// 1: Obtener todas las publicaciones (lote)
// → ¡Mejora significativa del rendimiento!
// ========== Ejemplo práctico ==========
const { ApolloServer } = require('apollo-server');
const DataLoader = require('dataloader');
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
// Crear nuevo DataLoader por solicitud
loaders: {
postLoader: new DataLoader(batchGetPosts),
userLoader: new DataLoader(batchGetUsers)
}
})
});
// Usar en resolver
const resolvers = {
User: {
posts: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
};
P4. ¿Versionado de REST API vs Evolución de esquema GraphQL?
R:
// ========== Versionado de REST API ==========
// Método 1: Versión en URL
GET /api/v1/users
GET /api/v2/users // Nueva versión
// Método 2: Versión en encabezado
GET /api/users
Accept: application/vnd.myapp.v1+json
// Problemas:
// - Mantener múltiples versiones
// - Se requiere actualización del cliente
// - Carga de soporte de versión antigua
// Ejemplo: Cambio de nombre de campo
// v1
{
"name": "Juan Pérez"
}
// v2
{
"fullName": "Juan Pérez" // name → fullName
}
// ========== Evolución de esquema GraphQL ==========
// Fácil agregar
type User {
id: Int!
name: String!
email: String! # Existente
phone: String # Agregado (nuevo campo)
address: Address # Agregado (nuevo tipo)
}
// Cambiar: Usar @deprecated
type User {
id: Int!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String! # Nuevo campo
email: String!
}
// El cliente se actualiza gradualmente
query {
user(id: 1) {
name # Todavía funciona (deprecated)
fullName # Usar nuevo campo
}
}
// Ventajas:
// - Endpoint único
// - No se necesita gestión de versiones
// - Migración gradual
// - El cliente actualiza cuando lo desee
P5. ¿Cómo manejar datos en tiempo real?
R:
// ========== REST API: Polling o SSE ==========
// 1. Polling
setInterval(async () => {
const response = await fetch('/api/messages');
const messages = await response.json();
updateUI(messages);
}, 3000); // Solicitar cada 3 segundos
// Desventajas: Ineficiente, carga del servidor
// 2. Server-Sent Events (SSE)
const eventSource = new EventSource('/api/messages/stream');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateUI(message);
};
// Desventajas: Solo comunicación unidireccional
// ========== GraphQL: Subscription ==========
// Configuración del servidor
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const typeDefs = gql`
type Message {
id: Int!
text: String!
userId: Int!
createdAt: String!
}
type Subscription {
messageAdded: Message!
}
type Mutation {
addMessage(text: String!): Message!
}
`;
const resolvers = {
Mutation: {
addMessage: (parent, args) => {
const message = {
id: messages.length + 1,
text: args.text,
userId: 1,
createdAt: new Date().toISOString()
};
messages.push(message);
// Notificar suscriptores
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});
return message;
}
},
Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};
// Cliente (React)
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
text
userId
createdAt
}
}
`;
function ChatRoom() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);
useEffect(() => {
if (data) {
console.log('Nuevo mensaje:', data.messageAdded);
addMessageToUI(data.messageAdded);
}
}, [data]);
return <div>{/* UI */}</div>;
}
// Ventajas:
// - Comunicación bidireccional en tiempo real
// - Basado en WebSocket
// - Eficiente
🎓 Próximos pasos
Ahora que entiendes REST API y GraphQL, intenta aprender:
- ¿Qué es API? (Próximamente) - Conceptos básicos de API
- Token JWT (Próximamente) - Autenticación de API
- ¿Qué es WebSocket? (Próximamente) - Comunicación en tiempo real
Práctica
# ========== Práctica REST API (Express) ==========
mkdir rest-api-demo
cd rest-api-demo
npm init -y
npm install express
# Después de escribir server.js
node server.js
# Probar
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Juan Pérez","email":"juan@example.com"}'
# ========== Práctica GraphQL (Apollo Server) ==========
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql
# Después de escribir server.js
node server.js
# Abrir GraphQL Playground
# http://localhost:4000
# ¡Ejecuta consultas en el navegador!
# ========== React + GraphQL ==========
npx create-react-app my-app
cd my-app
npm install @apollo/client graphql
# Después de configurar Apollo Client
npm start
🎬 Conclusión
REST API y GraphQL tienen sus pros y contras:
- REST API: Simple, familiar y caché fácil
- GraphQL: Flexible, eficiente y soporte en tiempo real
- Criterios de selección: Complejidad del proyecto, experiencia del equipo, requisitos de rendimiento
- Híbrido: Usar ambos enfoques según sea necesario
¡Construye grandes aplicaciones con un diseño de API adecuado! 🔄