GraphQL
"REST: el cliente recibe lo que el servidor decide. GraphQL: el cliente pide exactamente lo que necesita."
Qué vas a aprender en este capítulo
GraphQL es una alternativa a REST creada por Facebook en 2015 para resolver problemas específicos de su app móvil. Permite al cliente especificar qué campos quiere exactamente, evitando over-fetching y under-fetching. Este capítulo cubre los fundamentos y cuándo usarlo.
2.1 GraphQL vs REST
💡 Intuición
Problema con REST:
El endpoint GET /pedido/123 devuelve TODOS los campos del pedido — aunque solo necesites el total. Si querés también el nombre del mozo, necesitás OTRO request a GET /usuario/45. Cuatro requests para una pantalla = 4 viajes de red, mucho parsing, y lentitud especialmente en mobile.
Solución de GraphQL:
Una sola query especifica exactamente lo que necesitás:
query {
pedido(id: 123) {
total
mozo {
nombre
}
items {
cantidad
platillo {
nombre
}
}
}
}
Un solo request → un solo JSON con exactamente esos datos.
📐 Fundamento
| REST | GraphQL | |
|---|---|---|
| Endpoints | Múltiples (uno por recurso) | Uno (/graphql) |
| Sobre/sub fetching | Frecuente | Eliminado |
| Versionado | URL versions (/v1/, /v2/) |
Schema evolution |
| Caché HTTP | Natural (por URL) | Más complicado |
| Type safety | Externo (OpenAPI) | Nativa (Schema) |
| Aprendizaje | Bajo | Medio-alto |
| Tooling | Maduro | Maduro (GraphiQL, Apollo) |
| Ideal para | APIs públicas simples, microservicios | Apps con UIs complejas, multiple consumers |
Cuándo elegir GraphQL:
- App móvil donde cada KB transferido cuenta.
- Múltiples clientes (web, móvil, watch) con necesidades distintas.
- UI compleja con muchas relaciones jerárquicas.
- Equipo frontend independiente que necesita iterar sin esperar al backend.
Cuándo elegir REST:
- API pública para terceros (más conocido, mejor caché).
- Operaciones simples CRUD.
- Necesitás caché HTTP fuerte.
- Equipo pequeño, simplicidad importa más.
2.2 Schema GraphQL
📐 Fundamento
Schema Definition Language (SDL):
# Tipos escalares predefinidos: Int, Float, String, Boolean, ID
# Tipos custom
type Pedido {
id: ID! # ! = no nullable
mesaId: Int!
estado: EstadoPedido!
total: Float!
creadoEn: String!
mozo: Usuario! # relación
items: [ItemPedido!]! # lista no nullable de items no nullable
notas: String # nullable
}
enum EstadoPedido {
ABIERTO
LISTO
ENTREGADO
CANCELADO
}
type ItemPedido {
id: ID!
cantidad: Int!
platillo: Platillo!
}
type Platillo {
id: ID!
nombre: String!
precio: Float!
categoria: String
disponible: Boolean!
}
type Usuario {
id: ID!
nombre: String!
email: String!
pedidos: [Pedido!]! # un mozo puede tener muchos pedidos
}
# Inputs para mutations
input ItemInput {
platilloId: Int!
cantidad: Int!
}
input PedidoInput {
mesaId: Int!
items: [ItemInput!]!
notas: String
}
# Operaciones
type Query {
pedido(id: ID!): Pedido
pedidos(estado: EstadoPedido, limite: Int = 20): [Pedido!]!
menu: [Platillo!]!
}
type Mutation {
crearPedido(input: PedidoInput!): Pedido!
actualizarEstado(id: ID!, estado: EstadoPedido!): Pedido!
cancelarPedido(id: ID!): Boolean!
}
type Subscription {
pedidoCreado: Pedido! # tiempo real
pedidoCambiado(id: ID!): Pedido!
}
Queries — pedir datos:
query ObtenerPedido {
pedido(id: "123") {
total
estado
items {
cantidad
platillo {
nombre
precio
}
}
}
}
Resultado:
{
"data": {
"pedido": {
"total": 8.50,
"estado": "LISTO",
"items": [
{ "cantidad": 2, "platillo": { "nombre": "Pupusa de queso", "precio": 1.50 } }
]
}
}
}
Mutations — modificar datos:
mutation NuevoPedido {
crearPedido(input: {
mesaId: 3
items: [
{ platilloId: 1, cantidad: 2 }
{ platilloId: 5, cantidad: 1 }
]
}) {
id
total
estado
}
}
Variables (lo que se usa en producción):
mutation NuevoPedido($input: PedidoInput!) {
crearPedido(input: $input) {
id
total
}
}
// Variables enviadas separadamente
{ "input": { "mesaId": 3, "items": [...] } }
2.3 Implementación con Strawberry (Python)
📐 Fundamento
Strawberry es una librería moderna de GraphQL para Python con type hints nativos.
import strawberry
from typing import Optional
from enum import Enum
@strawberry.enum
class EstadoPedido(Enum):
ABIERTO = "ABIERTO"
LISTO = "LISTO"
ENTREGADO = "ENTREGADO"
@strawberry.type
class Platillo:
id: int
nombre: str
precio: float
categoria: Optional[str] = None
@strawberry.type
class ItemPedido:
id: int
cantidad: int
@strawberry.field
def platillo(self) -> Platillo:
# Resolver: cómo obtener el platillo de este item
return db.get_platillo(self.platillo_id)
@strawberry.type
class Pedido:
id: int
mesa_id: int
estado: EstadoPedido
total: float
@strawberry.field
def items(self) -> list[ItemPedido]:
return db.get_items_pedido(self.id)
@strawberry.field
def mozo(self) -> "Usuario":
return db.get_user(self.mozo_id)
@strawberry.type
class Usuario:
id: int
nombre: str
email: str
# Inputs
@strawberry.input
class ItemInput:
platillo_id: int
cantidad: int
@strawberry.input
class PedidoInput:
mesa_id: int
items: list[ItemInput]
notas: Optional[str] = None
# Queries
@strawberry.type
class Query:
@strawberry.field
def pedido(self, id: int) -> Optional[Pedido]:
return db.get_pedido(id)
@strawberry.field
def pedidos(self, estado: Optional[EstadoPedido] = None, limite: int = 20) -> list[Pedido]:
return db.list_pedidos(estado=estado, limit=limite)
@strawberry.field
def menu(self) -> list[Platillo]:
return db.get_menu_disponible()
# Mutations
@strawberry.type
class Mutation:
@strawberry.mutation
def crear_pedido(self, input: PedidoInput) -> Pedido:
return db.crear_pedido(input)
@strawberry.mutation
def actualizar_estado(self, id: int, estado: EstadoPedido) -> Pedido:
return db.actualizar_estado_pedido(id, estado)
schema = strawberry.Schema(query=Query, mutation=Mutation)
# Integrar con FastAPI
from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")
# Iniciar: uvicorn main:app
# Probar en http://localhost:8000/graphql (GraphiQL UI built-in)
2.4 El problema N+1 y DataLoader
💡 Intuición
GraphQL tiene un problema sutil: si pedís 100 pedidos y cada uno tiene un mozo, naïvely el servidor hace 1 query para pedidos + 100 queries para los mozos = 101 queries. Esto se llama el problema N+1.
DataLoader es la solución: agrupa requests del mismo "tick" del event loop y hace una sola query batch.
📐 Fundamento
Sin DataLoader (problemático):
@strawberry.field
def mozo(self) -> Usuario:
return db.get_user(self.mozo_id) # 1 query por pedido
Para query con 100 pedidos: 1 (pedidos) + 100 (mozos) = 101 queries.
Con DataLoader:
from strawberry.dataloader import DataLoader
# Función batch: recibe lista de IDs, devuelve lista en mismo orden
async def cargar_usuarios(ids: list[int]) -> list[Usuario]:
usuarios = db.execute(
"SELECT * FROM usuarios WHERE id = ANY(%s)",
(ids,)
)
# Mantener el orden de los IDs solicitados
by_id = {u.id: u for u in usuarios}
return [by_id.get(id) for id in ids]
usuario_loader = DataLoader(load_fn=cargar_usuarios)
@strawberry.type
class Pedido:
@strawberry.field
async def mozo(self) -> Usuario:
return await usuario_loader.load(self.mozo_id)
Ahora: 1 query (pedidos) + 1 query (todos los mozos en batch) = 2 queries. Mucho mejor.
Cómo funciona DataLoader internamente:
- Cada
load(id)se acumula en una cola. - Al final del tick del event loop (todos los resolvers ya pidieron lo suyo), DataLoader llama
cargar_usuarios([id1, id2, ...])con todos los IDs juntos. - Distribuye el resultado a cada caller.
- Cachea por la duración del request (no entre requests).
2.5 Subscriptions — datos en tiempo real
📐 Fundamento
GraphQL soporta subscriptions vía WebSockets para enviar datos en tiempo real al cliente.
import asyncio
from typing import AsyncGenerator
@strawberry.type
class Subscription:
@strawberry.subscription
async def pedido_creado(self) -> AsyncGenerator[Pedido, None]:
"""El cliente recibe un Pedido cada vez que se crea uno."""
async for pedido in db.subscribe_to_new_orders():
yield pedido
@strawberry.subscription
async def estado_pedido(self, pedido_id: int) -> AsyncGenerator[EstadoPedido, None]:
"""El cliente recibe el nuevo estado cuando cambia."""
async for nuevo_estado in db.subscribe_to_order_status(pedido_id):
yield nuevo_estado
schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)
Cliente JavaScript con Apollo Client:
import { ApolloClient, InMemoryCache, split, HttpLink, gql } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:8000/graphql' }));
const httpLink = new HttpLink({ uri: 'http://localhost:8000/graphql' });
const client = new ApolloClient({
link: split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink, // subscriptions van por WebSocket
httpLink // queries y mutations por HTTP
),
cache: new InMemoryCache()
});
// Suscribirse en React
function CocinaPantalla() {
const { data } = useSubscription(gql`
subscription { pedidoCreado { id mesaId items { platillo { nombre } } } }
`);
return <div>Nuevo pedido: {data?.pedidoCreado.id}</div>;
}
2.6 Ejercicios
✏️ Ejercicio 2.1 — Diseñar schema GraphQL
Diseñá el schema GraphQL para un sistema de delivery con: clientes, repartidores, pedidos y direcciones de entrega. Las operaciones necesarias:
- Cliente consulta sus pedidos pasados.
- Repartidor ve los pedidos asignados a él.
- Cliente crea un pedido nuevo.
- Sistema notifica al repartidor cuando le asignan un pedido.
Solución
type Cliente {
id: ID!
nombre: String!
email: String!
telefono: String
direcciones: [Direccion!]!
pedidos(limite: Int = 20): [Pedido!]!
}
type Repartidor {
id: ID!
nombre: String!
vehiculo: String
pedidosActivos: [Pedido!]!
pedidosHoy: [Pedido!]!
}
type Direccion {
id: ID!
calle: String!
ciudad: String!
referencia: String
latitud: Float!
longitud: Float!
}
enum EstadoDelivery {
PENDIENTE
PREPARANDO
EN_CAMINO
ENTREGADO
CANCELADO
}
type Pedido {
id: ID!
estado: EstadoDelivery!
total: Float!
cliente: Cliente!
repartidor: Repartidor
direccionEntrega: Direccion!
fechaCreacion: String!
fechaEntrega: String
items: [ItemPedido!]!
}
type ItemPedido {
cantidad: Int!
platillo: Platillo!
precio: Float!
}
input PedidoInput {
direccionId: ID!
items: [ItemInput!]!
notas: String
}
input ItemInput {
platilloId: ID!
cantidad: Int!
}
type Query {
miPerfil: Cliente!
misPedidos(limite: Int = 20): [Pedido!]!
pedidosAsignados: [Pedido!]! # para repartidor
menu: [Platillo!]!
}
type Mutation {
crearPedido(input: PedidoInput!): Pedido!
actualizarEstadoPedido(id: ID!, estado: EstadoDelivery!): Pedido!
}
type Subscription {
# Repartidor se suscribe para recibir nuevos pedidos
nuevoPedidoAsignado: Pedido!
# Cliente se suscribe para ver actualizaciones de su pedido
estadoPedido(pedidoId: ID!): EstadoDelivery!
}
2.7 Para profundizar
- graphql.org — documentación oficial.
- How To GraphQL (howtographql.com) — tutorial completo.
- Apollo Server / Apollo Client — el ecosistema GraphQL más popular.
- Siguiente: Frontend moderno con React.
Definiciones nuevas: GraphQL, schema, SDL, query, mutation, subscription, resolver, type system, scalar, input type, N+1 problem, DataLoader, batch loading, Strawberry, Apollo Client, GraphiQL.