GraphQL vs REST: diferencias, ventajas y cuándo elegir cada uno

D
DanisCh
(Actualizado: ) • 12 min de lectura
GraphQL vs REST: diferencias, ventajas y cuándo elegir cada uno
NoSQL

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

AspectoRESTGraphQL
EndpointsMúltiples (uno por recurso)Uno solo (/graphql)
Quién decide los datos devueltosEl servidor (campo fijo por endpoint)El cliente (pide exactamente lo que necesita)
Over-fetchingFrecuente (datos innecesarios)Eliminado por diseño
Under-fetchingFrecuente (múltiples peticiones)Eliminado por diseño
Tipado y schemaOpcional (OpenAPI/Swagger)Obligatorio y central
VersionadoVersiones de URL (/v1, /v2)Evolución sin versiones (deprecar campos)
Caché HTTPNativo (GET es cacheado por navegadores y CDNs)Más complejo (todo va por POST, hay que gestionar caché en cliente)
Curva de aprendizajeBajaMedia-alta
Herramientas y ecosistemaMaduro y universalMaduro pero más específico
Tiempo realWebhooks, SSE, WebSockets (separados)Subscriptions (integrado en el protocolo)
Problema N+1 en servidorNo aplicaNecesita DataLoader
Idóneo paraAPIs públicas, microservicios, sistemas simplesApps 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.
Etiquetas: API REST Rest GraphQL

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a