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:

  1. Cada load(id) se acumula en una cola.
  2. 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.
  3. Distribuye el resultado a cada caller.
  4. 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:

  1. Cliente consulta sus pedidos pasados.
  2. Repartidor ve los pedidos asignados a él.
  3. Cliente crea un pedido nuevo.
  4. Sistema notifica al repartidor cuando le asignan un pedido.

2.7 Para profundizar


Definiciones nuevas: GraphQL, schema, SDL, query, mutation, subscription, resolver, type system, scalar, input type, N+1 problem, DataLoader, batch loading, Strawberry, Apollo Client, GraphiQL.