TechCorp ya tiene sus dos canales principales: REST/JSON para las llamadas síncronas (03-01) y RabbitMQ para los eventos (03-02). Con eso se puede construir todo el sistema, y de hecho es lo que haremos en el módulo 4. Pero conviene conocer dos alternativas que resuelven problemas concretos que REST/JSON resuelve mal: gRPC, para llamadas internas de alto volumen con contrato tipado y binario, y GraphQL, para que un front-end pida exactamente los datos que necesita de varios servicios en una sola consulta. No son modas ni sustitutos de REST: son herramientas con un nicho claro, y saber cuál es evita tanto ignorarlas como usarlas donde no tocan.
En esta lección veremos qué limitaciones de REST/JSON motivan cada alternativa; gRPC con Protocol Buffers (un .proto real del servicio de Inventario), HTTP/2, los cuatro tipos de llamada, y un servidor y un cliente mínimos en Node.js con @grpc/grpc-js explicados línea a línea; GraphQL con un esquema de pedidos y productos, resolvers, el problema N+1 y DataLoader; una tabla comparativa de REST, gRPC, GraphQL y mensajería; y la decisión de TechCorp sobre dónde encaja cada uno. El gateway y el BFF (donde GraphQL brillaría) son de 03-04, y el versionado de .proto y esquemas GraphQL, de 03-06.
Contenido
- Limitaciones de REST/JSON que motivan alternativas
- gRPC: qué es y cómo funciona
- Protocol Buffers: el contrato
.protode Inventario - Servidor y cliente gRPC en Node.js
- Errores en gRPC y cuándo usarlo
- GraphQL: esquema, consultas y mutaciones
- Resolvers en Node.js
- El problema N+1 y DataLoader
- Cuándo usar GraphQL y sus riesgos
- Comparativa: REST, gRPC, GraphQL y mensajería
- La decisión de TechCorp
- Limitaciones de REST/JSON que motivan alternativas
REST sobre JSON es el estándar de facto porque es simple, universal y legible. Precisamente por eso tiene tres carencias que se notan a medida que un sistema crece:
| Limitación | En qué consiste | Dónde lo sufriría TechCorp |
|---|---|---|
| Verbosidad y coste de serialización | JSON es texto: cada campo repite su nombre, los números viajan como cadenas de dígitos, y parsearlo cuesta CPU. HTTP/1.1 abre conexiones y envía cabeceras completas en cada petición. | Pedidos → Inventario si pasara a síncrono: miles de llamadas por minuto en el pico, cada una con el mismo JSON de líneas. |
| Ausencia de contrato fuerte | OpenAPI describe la API, pero es un documento aparte del código: nada impide que el servidor devuelva precio como cadena un día. Los clientes se escriben a mano o se generan de forma opcional. |
Un cambio en Catálogo que rompe el traductorProducto de Pedidos se descubre en producción o, con suerte, en 04-05. |
| Over-fetching y under-fetching | Un endpoint devuelve una forma fija. Si el front necesita menos, sobra (over-fetching); si necesita datos de dos recursos, hace dos peticiones (under-fetching). | La pantalla de "detalle de pedido" de la app móvil necesita el pedido, los productos con sus imágenes y el estado del pago: tres llamadas, o un endpoint a medida por pantalla. |
gRPC ataca las dos primeras (binario compacto, HTTP/2 y contrato generado desde un .proto). GraphQL ataca la tercera (el cliente declara la forma exacta que quiere). Ninguna ataca las tres, y ninguna resuelve lo que la mensajería resuelve (el acoplamiento temporal).
- gRPC: qué es y cómo funciona
gRPC es un framework de llamada a procedimiento remoto (RPC) creado por Google. Sus ingredientes:
- Contrato primero. Se escribe un fichero
.protoque define los servicios, sus métodos y los mensajes que intercambian. De él se genera el código de servidor y de cliente en el lenguaje que sea (Node.js, Go, Java...). El contrato no es documentación: es la fuente. - Protocol Buffers (protobuf) como formato de serialización: binario, compacto (un
int32ocupa 1-5 bytes; en JSON,"cantidad": 2ocupa 13), y con esquema (el receptor sabe qué tipo tiene cada campo sin adivinar). - HTTP/2 como transporte: una sola conexión TCP multiplexa muchas llamadas en paralelo, las cabeceras se comprimen, y hay streaming nativo en ambos sentidos.
- Cuatro tipos de llamada:
| Tipo | Cliente envía | Servidor devuelve | Ejemplo en TechCorp |
|---|---|---|---|
| Unaria | 1 mensaje | 1 mensaje | ReservarStock(SolicitudReserva) → Reserva |
| Streaming de servidor | 1 mensaje | Flujo de N mensajes | SeguirDisponibilidad(productoId) → stream Disponibilidad (el cliente recibe cada cambio de stock) |
| Streaming de cliente | Flujo de N mensajes | 1 mensaje | ImportarStock(stream Movimiento) → Resumen (carga masiva desde el almacén) |
| Bidireccional | Flujo | Flujo | Chat de soporte en tiempo real (fuera del alcance de TechCorp) |
La forma mental correcta: gRPC se parece a llamar a una función que vive en otro proceso, con tipos comprobados en compilación (o en carga, en Node.js). REST se parece a manipular documentos. Por eso gRPC encaja en llamadas internas servicio-a-servicio y REST en APIs públicas.
- Protocol Buffers: el contrato
.proto de Inventario
.proto de InventarioEn 03-01 diseñamos POST /reservas como contrato REST de Inventario "por si la relación Pedidos↔Inventario pasa a síncrona". Este es el mismo contrato en gRPC:
// inventario.proto
syntax = "proto3";
package techcorp.inventario.v1;
// El servicio: cada rpc es un método remoto
service ServicioInventario {
// Unaria: reservar stock para un pedido (equivalente a POST /reservas)
rpc ReservarStock (SolicitudReserva) returns (Reserva);
// Unaria: consultar disponibilidad de varios productos (lote, como GET /productos?ids=)
rpc ConsultarDisponibilidad (SolicitudDisponibilidad) returns (RespuestaDisponibilidad);
// Streaming de servidor: recibir cambios de disponibilidad de un producto
rpc SeguirDisponibilidad (SolicitudSeguimiento) returns (stream Disponibilidad);
}
// Los mensajes: cada campo tiene tipo, nombre y NÚMERO. El número es lo que viaja por la red;
// el nombre es solo para el código. Por eso nunca se reutiliza un número (03-06).
message LineaReserva {
string producto_id = 1; // "p-501"
int32 cantidad = 2;
}
message SolicitudReserva {
string pedido_id = 1; // "ped-88213"; sirve también de clave de idempotencia
repeated LineaReserva lineas = 2; // repeated = lista
int32 expira_en_segundos = 3; // 900
}
enum EstadoReserva {
ESTADO_RESERVA_SIN_ESPECIFICAR = 0; // proto3 exige un valor 0 por defecto
ACTIVA = 1;
CONSUMIDA = 2;
LIBERADA = 3;
}
message Reserva {
string id = 1; // "res-4471"
string pedido_id = 2;
EstadoReserva estado = 3;
string expira_en = 4; // ISO 8601; hay un tipo Timestamp estándar, pero así es más didáctico
repeated LineaReserva lineas = 5;
}
message SolicitudDisponibilidad {
repeated string producto_ids = 1;
}
message Disponibilidad {
string producto_id = 1;
int32 unidades_disponibles = 2;
bool disponible = 3;
}
message RespuestaDisponibilidad {
repeated Disponibilidad productos = 1;
}
message SolicitudSeguimiento {
string producto_id = 1;
}Cómo leerlo:
syntax = "proto3"es la versión actual del lenguaje.packageda un espacio de nombres (y, conv1, deja hueco para versionar, tema de 03-06).serviceagrupa losrpc. Cadarpctiene un mensaje de entrada y uno de salida;streamdelante de uno de ellos lo convierte en flujo.messagees como unastruct. Cada campo lleva tipo (string,int32,bool,repeated X, otromessage,enum), nombre ensnake_case(la convención protobuf; el código generado lo convierte acamelCaseen JavaScript si se le pide) y un número de campo único dentro del mensaje. Ese número es la identidad del campo en el binario: cambiar el nombre no rompe nada; cambiar el número, sí.- En proto3 todos los campos son opcionales y tienen valor por defecto (
"",0,false, lista vacía). No haynull: hay que decidir cómo representar "ausente" (conoptional, disponible desde protobuf 3.15, o con un mensaje envolvente). - Los
enumdeben tener un valor0, que es el defecto; por convenio se llama*_SIN_ESPECIFICARpara distinguirlo de un valor real.
Fíjate en que este .proto es el contrato entre Pedidos e Inventario: si el equipo de Luis y el de Inventario (que en 02-01 vimos que son el mismo equipo) lo acuerdan, cada uno genera su lado y trabaja en paralelo.
- Servidor y cliente gRPC en Node.js
En Node.js hay dos maneras de usar un .proto: generar código estático con protoc y el plugin de JavaScript, o cargarlo dinámicamente en tiempo de ejecución con @grpc/proto-loader. La segunda es la más sencilla para aprender y la que usaremos; en producción TechCorp usaría también la carga dinámica salvo que necesite tipos TypeScript generados.
Servidor (dentro de servicio-inventario; solo la parte gRPC, el resto del servicio es del módulo 4):
// grpc/servidorInventario.js
const path = require('node:path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// 1. Cargar y "compilar" el .proto en memoria.
// keepCase:false convierte producto_id → productoId en los objetos JS.
// longs/enums/defaults controlan cómo se representan tipos; estos valores son los habituales.
const definicion = protoLoader.loadSync(path.join(__dirname, 'inventario.proto'), {
keepCase: false, longs: String, enums: String, defaults: true, oneofs: true
});
// 2. Convertir la definición en objetos gRPC utilizables; navegamos hasta el paquete
const proto = grpc.loadPackageDefinition(definicion).techcorp.inventario.v1;
// 3. Implementación de cada rpc. La firma es siempre (llamada, callback) para unarias.
// llamada.request es el mensaje de entrada ya deserializado; callback(error, respuesta) responde.
const implementacion = {
ReservarStock: async (llamada, callback) => {
const { pedidoId, lineas, expiraEnSegundos } = llamada.request;
try {
// La lógica real (transacción sobre stock/reservas, publicar stock.reservado) es del módulo 4
const reserva = await casosDeUso.reservarStock({ pedidoId, lineas, expiraEnSegundos });
callback(null, {
id: reserva.id, pedidoId: reserva.pedidoId, estado: 'ACTIVA',
expiraEn: reserva.expiraEn.toISOString(), lineas: reserva.lineas
});
} catch (err) {
// 4. Los errores se comunican con códigos gRPC, no con excepciones (apartado 5)
if (err.codigo === 'SIN_STOCK') {
return callback({ code: grpc.status.FAILED_PRECONDITION, details: `SIN_STOCK: ${err.message}` });
}
callback({ code: grpc.status.INTERNAL, details: 'Error interno' });
}
},
ConsultarDisponibilidad: async (llamada, callback) => {
const { productoIds } = llamada.request;
if (productoIds.length === 0 || productoIds.length > 100) {
return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'Entre 1 y 100 ids' });
}
const filas = await repositorioStock.disponibilidadDe(productoIds);
callback(null, {
productos: filas.map(f => ({ productoId: f.productoId, unidadesDisponibles: f.disponibles, disponible: f.disponibles > 0 }))
});
},
// 5. Streaming de servidor: no hay callback; se escribe en 'llamada' tantas veces como haga falta y se cierra con end()
SeguirDisponibilidad: (llamada) => {
const { productoId } = llamada.request;
const cancelar = notificadorStock.suscribir(productoId, (disp) => {
llamada.write({ productoId, unidadesDisponibles: disp, disponible: disp > 0 });
});
llamada.on('cancelled', cancelar); // el cliente cortó: dejar de enviar
}
};
// 6. Crear el servidor, registrar el servicio con su implementación y escuchar.
// createInsecure() = sin TLS; válido dentro del clúster para aprender. TLS/mTLS se ve en 07-02.
function arrancarGrpc(puerto = 50051) {
const servidor = new grpc.Server();
servidor.addService(proto.ServicioInventario.service, implementacion);
servidor.bindAsync(`0.0.0.0:${puerto}`, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) throw err;
console.log(`gRPC ServicioInventario escuchando en ${puerto}`);
});
return servidor;
}
module.exports = { arrancarGrpc };Cliente (dentro de servicio-pedidos):
// grpc/inventarioCliente.js
const path = require('node:path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// 1. Mismo .proto, mismas opciones: el contrato es compartido (en un paquete npm o un repo de contratos)
const definicion = protoLoader.loadSync(path.join(__dirname, 'inventario.proto'), {
keepCase: false, longs: String, enums: String, defaults: true, oneofs: true
});
const proto = grpc.loadPackageDefinition(definicion).techcorp.inventario.v1;
// 2. Un stub: objeto con un método por rpc. La dirección es el nombre estable del servicio (03-05).
const INVENTARIO_GRPC = process.env.INVENTARIO_GRPC ?? 'servicio-inventario:50051';
const stub = new proto.ServicioInventario(INVENTARIO_GRPC, grpc.credentials.createInsecure());
// 3. Los stubs usan callbacks; los envolvemos en promesas para usarlos con async/await
function reservarStock(solicitud, { requestId } = {}) {
return new Promise((resolve, reject) => {
// 4. Metadata = cabeceras gRPC. Propagamos X-Request-Id igual que en REST.
const metadata = new grpc.Metadata();
if (requestId) metadata.set('x-request-id', requestId);
// 5. deadline = timeout absoluto: si en 2 s no hay respuesta, error DEADLINE_EXCEEDED
const deadline = new Date(Date.now() + 2000);
stub.ReservarStock(solicitud, metadata, { deadline }, (err, respuesta) => {
if (err) return reject(err); // err.code es un grpc.status; err.details el texto
resolve(respuesta);
});
});
}
module.exports = { reservarStock };Y su uso, con el pedido de siempre:
try {
const reserva = await reservarStock({
pedidoId: 'ped-88213',
lineas: [{ productoId: 'p-501', cantidad: 1 }, { productoId: 'p-777', cantidad: 2 }],
expiraEnSegundos: 900
}, { requestId: 'req-01J4ZK9X2M' });
console.log(reserva.id, reserva.estado); // res-4471 ACTIVA
} catch (err) {
if (err.code === grpc.status.FAILED_PRECONDITION) { /* SIN_STOCK → cancelar pedido */ }
else if (err.code === grpc.status.DEADLINE_EXCEEDED) { /* Inventario no responde → 503 */ }
else throw err;
}Lo esencial: el .proto es el único fichero compartido; servidor y cliente lo cargan y obtienen objetos tipados; los nombres de método y de campo salen del contrato, no de una URL escrita a mano; y el deadline es el equivalente del AbortSignal.timeout de 03-01.
- Errores en gRPC y cuándo usarlo
gRPC no usa códigos HTTP: tiene los suyos, más pocos y más precisos. Los que TechCorp mapea:
| Código gRPC | Equivalente REST | Cuándo |
|---|---|---|
OK (0) |
200 | Éxito |
INVALID_ARGUMENT (3) |
400 / 422 | Petición malformada o valores inválidos |
NOT_FOUND (5) |
404 | Recurso inexistente |
ALREADY_EXISTS (6) |
409 | Ya existe (reserva duplicada sin idempotencia) |
FAILED_PRECONDITION (9) |
409 | El estado no permite la operación: SIN_STOCK, transición inválida |
PERMISSION_DENIED (7) / UNAUTHENTICATED (16) |
403 / 401 | Autorización / autenticación |
RESOURCE_EXHAUSTED (8) |
429 | Límite superado |
DEADLINE_EXCEEDED (4) |
503/504 en el llamante | Timeout |
UNAVAILABLE (14) |
503 | Servidor caído o arrancando; el cliente puede reintentar |
INTERNAL (13) |
500 | Fallo no controlado |
El código de negocio (SIN_STOCK) viaja en details o, mejor, en un mensaje de error estructurado (google.rpc.Status con detalles tipados); para TechCorp basta con el prefijo en details y un mapeo en el cliente.
Cuándo usar gRPC:
- Comunicación interna servicio-a-servicio con alto volumen o baja latencia exigida: la serialización binaria y HTTP/2 se notan a partir de miles de llamadas por segundo.
- Cuando quieres un contrato fuerte y código generado en varios lenguajes (equipos con Go, Java y Node.js).
- Streaming (progreso, seguimiento en tiempo real).
Cuándo no:
- APIs públicas consumidas por navegadores: los navegadores no hablan gRPC nativo (existe gRPC-Web con un proxy, pero añade piezas), y los desarrolladores externos esperan REST.
- Cuando la legibilidad importa más que el rendimiento: no se puede hacer
curly leer la respuesta. - Equipos pequeños sin problema de rendimiento: es complejidad sin retorno.
Para TechCorp: Pedidos → Inventario es la candidata natural si algún día se hace síncrona (partnership de 02-03, mismo equipo, llamadas frecuentes). Hoy va por eventos y seguirá así; el .proto queda diseñado.
- GraphQL: esquema, consultas y mutaciones
GraphQL es un lenguaje de consulta para APIs y un runtime que las ejecuta. La idea central: el servidor publica un esquema tipado de todo lo que se puede pedir, y el cliente envía una consulta que describe exactamente la forma de la respuesta que quiere. Un solo endpoint (POST /graphql), un solo viaje, ni un campo de más ni de menos.
Esquema (SDL, Schema Definition Language) para una vista de pedidos que combina datos de Pedidos y de Catálogo:
# esquema.graphql
type Producto {
id: ID!
nombre: String!
precio: Float!
moneda: String!
imagenUrl: String
disponible: Boolean!
}
type LineaPedido {
productoId: ID!
nombre: String! # congelado en el pedido (02-03)
cantidad: Int!
precioUnitario: Float!
producto: Producto # ¡datos VIVOS de Catálogo! (imagen, disponibilidad actual)
}
enum EstadoPedido { PENDIENTE STOCK_RESERVADO PAGADO CONFIRMADO CANCELADO }
type Direccion { calle: String!, codigoPostal: String!, ciudad: String!, pais: String! }
type Pedido {
id: ID!
estado: EstadoPedido!
clienteId: ID!
lineas: [LineaPedido!]!
total: Float!
direccionEnvio: Direccion!
creadoEn: String!
}
type Query {
pedido(id: ID!): Pedido
pedidosDeCliente(clienteId: ID!, limite: Int = 20, cursor: String): [Pedido!]!
productos(ids: [ID!]!): [Producto!]!
}
input LineaEntrada { productoId: ID!, cantidad: Int! }
input DireccionEntrada { calle: String!, codigoPostal: String!, ciudad: String!, pais: String! }
type Mutation {
crearPedido(clienteId: ID!, lineas: [LineaEntrada!]!, direccionEnvio: DireccionEntrada!): Pedido!
}Cómo leerlo: type define objetos con campos tipados (! = no nulo; [X!]! = lista no nula de elementos no nulos); Query son las lecturas y Mutation las escrituras (ambas son solo tipos especiales); input son tipos para argumentos; enum como en protobuf. Fíjate en LineaPedido.producto: un campo que cruza servicios. Es lo que REST no da sin un endpoint a medida.
Consulta del cliente móvil para la pantalla de detalle de pedido:
query DetallePedido($id: ID!) {
pedido(id: $id) {
id
estado
total
lineas {
nombre
cantidad
precioUnitario
producto { imagenUrl disponible }
}
}
}Con {"id": "ped-88213"} como variables, la respuesta tiene exactamente esa forma:
{
"data": {
"pedido": {
"id": "ped-88213",
"estado": "CONFIRMADO",
"total": 79.7,
"lineas": [
{ "nombre": "Auriculares BT X200", "cantidad": 1, "precioUnitario": 59.9, "producto": { "imagenUrl": "https://cdn.techcorp.example/p-501.webp", "disponible": true } },
{ "nombre": "Cable USB-C 2 m", "cantidad": 2, "precioUnitario": 9.9, "producto": { "imagenUrl": "https://cdn.techcorp.example/p-777.webp", "disponible": true } }
]
}
}
}Ni clienteId, ni direccionEnvio, ni creadoEn: no se pidieron. Y una mutación:
mutation {
crearPedido(
clienteId: "c-1024",
lineas: [{ productoId: "p-501", cantidad: 1 }, { productoId: "p-777", cantidad: 2 }],
direccionEnvio: { calle: "Gran Vía 12", codigoPostal: "28013", ciudad: "Madrid", pais: "ES" }
) { id estado }
}
- Resolvers en Node.js
El esquema dice qué se puede pedir; los resolvers dicen cómo se obtiene cada campo. Un resolver es una función (padre, args, contexto, info) por campo; los que no se definen se resuelven por defecto leyendo la propiedad del mismo nombre en el objeto padre. Usaremos graphql-yoga, un servidor GraphQL ligero que se monta sobre Express (Apollo Server es la alternativa más conocida y equivalente para lo que hacemos aquí):
// graphql/servidor.js (esto vivirá en el BFF móvil de 03-04, no en Pedidos)
const { createSchema, createYoga } = require('graphql-yoga');
const { readFileSync } = require('node:fs');
const express = require('express');
// Clientes REST de los servicios internos (los de 03-01)
const pedidosApi = require('../clientes/pedidosCliente'); // GET /pedidos/{id}, POST /pedidos
const catalogoApi = require('../clientes/catalogoCliente'); // GET /productos?ids=
const typeDefs = readFileSync(require.resolve('./esquema.graphql'), 'utf8');
const resolvers = {
Query: {
// 1. Resolver raíz: args trae los argumentos de la consulta; contexto, lo que inyectamos por petición
pedido: (_padre, { id }, contexto) => pedidosApi.obtenerPedido(id, { requestId: contexto.requestId }),
productos: (_padre, { ids }, contexto) => catalogoApi.obtenerProductos(ids, { requestId: contexto.requestId })
},
Mutation: {
crearPedido: (_padre, args, contexto) =>
pedidosApi.crearPedido(args, { requestId: contexto.requestId, claveIdempotencia: contexto.claveIdempotencia })
},
LineaPedido: {
// 2. Resolver de campo: 'padre' es la línea ya obtenida por el resolver de arriba.
// Solo se ejecuta si la consulta pidió 'producto'. AQUÍ nace el problema N+1 (apartado 8).
producto: (linea, _args, contexto) => contexto.cargadorProductos.load(linea.productoId)
}
};
const yoga = createYoga({
schema: createSchema({ typeDefs, resolvers }),
// 3. El contexto se crea por petición: request id, DataLoader nuevo, y más adelante el usuario (07-01)
context: ({ request }) => ({
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
claveIdempotencia: request.headers.get('idempotency-key'),
cargadorProductos: crearCargadorProductos(request) // apartado 8
}),
graphqlEndpoint: '/graphql'
});
const app = express();
app.use(yoga.graphqlEndpoint, yoga); // POST /graphql (y GET con la interfaz GraphiQL para probar)La cadena para DetallePedido: el motor llama a Query.pedido (una llamada REST a Pedidos), obtiene el pedido con sus dos líneas, y para cada línea llama a LineaPedido.producto. Sin más cuidado, eso son dos llamadas a Catálogo (una por línea) además de la de Pedidos; con 20 líneas, veinte. Es el N+1 que ya evitamos en REST con ?ids=; en GraphQL hay que evitarlo con DataLoader.
- El problema N+1 y DataLoader
DataLoader es una pequeña librería (creada por Facebook junto a GraphQL) que hace dos cosas: agrupa (batching) todas las llamadas a .load(id) que ocurren en el mismo tick del bucle de eventos en una sola llamada a tu función de lote, y cachea por petición para no pedir dos veces el mismo id.
// graphql/cargadores.js
const DataLoader = require('dataloader');
const catalogoApi = require('../clientes/catalogoCliente');
function crearCargadorProductos(request) {
const requestId = request.headers.get('x-request-id');
// La función de lote recibe TODOS los ids pedidos en este tick y debe devolver
// un array del MISMO tamaño y en el MISMO orden (null donde no exista)
return new DataLoader(async (ids) => {
const productos = await catalogoApi.obtenerProductos([...ids], { requestId }); // UNA llamada: GET /productos?ids=p-501,p-777
const porId = new Map(productos.map(p => [p.id, p]));
return ids.map(id => porId.get(id) ?? null);
}, { maxBatchSize: 100 }); // respetamos el límite de lote del contrato de Catálogo (03-01)
}
module.exports = { crearCargadorProductos };Con esto, la consulta DetallePedido hace exactamente dos llamadas REST (Pedidos y Catálogo) sea cual sea el número de líneas. Dos reglas: el DataLoader se crea por petición (en el contexto), nunca global (la caché filtraría datos entre usuarios y no se refrescaría), y su función de lote debe respetar el orden de los ids.
- Cuándo usar GraphQL y sus riesgos
Cuándo sí:
- Agregación para front-ends: una app o web con muchas pantallas que combinan datos de varios servicios y evolucionan a distinto ritmo. El cliente pide lo que necesita sin que el back-end cree un endpoint por pantalla. Es el caso del BFF que veremos en 03-04: GraphQL es una forma excelente de implementarlo.
- Clientes con ancho de banda limitado (móvil) donde el over-fetching cuesta.
- Cuando el equipo de front y el de back quieren desacoplar su ritmo: el back publica el esquema; el front decide qué consulta.
Cuándo no:
- Entre servicios internos: los servicios no necesitan flexibilidad de forma; necesitan contratos estables y simples (REST o gRPC) o eventos.
- Como fachada única de todo el sistema ("un grafo para gobernarlos a todos"): acaba siendo un monolito de esquema mantenido por un equipo cuello de botella.
Riesgos que hay que gestionar desde el primer día:
| Riesgo | Por qué | Mitigación |
|---|---|---|
| Consultas costosas | El cliente puede pedir pedidosDeCliente { lineas { producto { ... } } } con anidamiento arbitrario, o listas enormes |
Límite de profundidad y de "complejidad" (cada campo suma; se rechazan consultas por encima de un umbral); limite máximo en listas; timeouts |
| Caché HTTP inutilizada | Todo es POST /graphql: los CDN y navegadores no cachean por URL |
Caché en el cliente (Apollo Client, urql), persisted queries (el cliente envía un hash de una consulta registrada y se puede usar GET), caché por campo en el servidor |
| N+1 | Un resolver por campo, ingenuo | DataLoader siempre que un campo cruce servicios |
| Errores parciales | Una consulta puede devolver data con partes en null y una lista errors; los clientes no acostumbrados lo ignoran |
Tratar errors siempre; decidir por campo si es anulable (Producto en LineaPedido es anulable a propósito: si Catálogo falla, el pedido se muestra igual) |
| Autorización por campo | Ya no hay "una ruta = un permiso" | Comprobar permisos en resolvers o con directivas; se ve en 07-01 |
- Comparativa: REST, gRPC, GraphQL y mensajería
| Criterio | REST/JSON | gRPC | GraphQL | Mensajería (RabbitMQ) |
|---|---|---|---|---|
| Formato | JSON (texto) | Protobuf (binario) | JSON (texto) | El que quieras; TechCorp: JSON |
| Contrato | OpenAPI (opcional, aparte del código) | .proto (obligatorio, genera código) |
Esquema SDL (obligatorio, introspectivo) | Esquema de eventos (AsyncAPI, opcional) |
| Transporte | HTTP/1.1 o 2 | HTTP/2 | HTTP (normalmente POST) | AMQP |
| Sincronía | Síncrono | Síncrono (+ streaming) | Síncrono (+ subscriptions) | Asíncrono |
| Acoplamiento temporal | Sí | Sí | Sí | No |
| Forma de la respuesta | Fija por endpoint | Fija por método | La decide el cliente | Fija por evento |
| Legibilidad / depuración | Excelente (curl) |
Baja (binario; hace falta grpcurl) |
Buena (GraphiQL) | Media (consola del broker) |
| Caché HTTP | Nativa (ETag, Cache-Control) |
No | Difícil | No aplica |
| Navegadores | Nativo | Vía gRPC-Web + proxy | Nativo | No (vía WebSocket/SSE en un servicio) |
| Rendimiento | Bueno | Excelente | Bueno (depende de resolvers) | Excelente para desacoplar y absorber picos |
| Casos de uso | APIs públicas, CRUD, llamadas internas sencillas | Interno de alto volumen, políglota, streaming | Agregación para front-ends, BFF | Eventos de negocio, sagas, integración |
| Herramientas | Todo el ecosistema HTTP | protoc, grpcurl, Buf |
Apollo, Yoga, GraphiQL, DataLoader | RabbitMQ, amqplib, consola de gestión |
| Curva de aprendizaje | Baja | Media | Media (alta para hacerlo bien: N+1, complejidad) | Media (nuevo modelo mental) |
- La decisión de TechCorp
Marta y los líderes de equipo fijan la política de protocolos:
| Ámbito | Protocolo | Motivo |
|---|---|---|
| API pública (web, app, socios) a través del gateway 8080 | REST/JSON con OpenAPI | Universal, cacheable, depurable; es lo que esperan los consumidores externos |
| Entre servicios, por defecto | Eventos en RabbitMQ | Sin acoplamiento temporal; es la saga de 02-05 |
| Entre servicios, cuando hace falta respuesta inmediata | REST/JSON (Pedidos → Catálogo, Pedidos → Clientes) | Volumen moderado; reutiliza los contratos de 03-01 y las mismas herramientas |
| Pedidos ↔ Inventario | Eventos hoy; gRPC candidato si pasa a síncrono | Mismo equipo, partnership, potencial alto volumen; el .proto ya está diseñado |
| BFF de la app móvil | GraphQL candidato | Pantallas que agregan Pedidos + Catálogo + Pagos; ancho de banda móvil; se decide en 03-04 |
La regla que resume la decisión: no se introduce un protocolo nuevo hasta que hay un problema medido que REST + eventos no resuelven. gRPC y GraphQL quedan como herramientas conocidas y con un lugar reservado, no como decisiones tomadas por moda.
Errores Comunes y Consejos
- Adoptar gRPC "porque es más rápido" sin medir. Si Pedidos → Catálogo hace 50 llamadas por segundo, JSON no es tu problema. Mide primero (06-04).
- Reutilizar números de campo en un
.protoo cambiar el tipo de un campo existente. El receptor antiguo interpretará bytes con el significado equivocado. Se profundiza en 03-06; de momento: números nuevos siempre. - Olvidar el
deadlineen el cliente gRPC. Igual quefetchsinsignal: la llamada puede colgarse indefinidamente. - GraphQL sin DataLoader. El N+1 en GraphQL es silencioso: en desarrollo, con dos líneas, no se nota; en producción, con veinte, es una tormenta de peticiones a Catálogo.
- GraphQL sin límites de complejidad. Es exponer una consulta arbitraria a Internet. Profundidad máxima y coste máximo desde el primer despliegue.
- DataLoader global (compartido entre peticiones). Cachea datos de un usuario para otro y no se refresca. Uno por petición, en el contexto.
- Usar GraphQL entre servicios internos. Añade una capa de resolución y pierde la simplicidad de REST/gRPC sin ganar nada: los servicios no necesitan elegir la forma.
- Ignorar
errorsen la respuesta GraphQL.datapuede venir connullparciales y el error estar en otra lista. Los clientes deben mirar ambos.
Ejercicios
Ejercicio 1. Añade al .proto de Inventario un método unario LiberarReserva que reciba el id de la reserva y un motivo (SIN_STOCK, PAGO_RECHAZADO, TIMEOUT_PAGO, CANCELACION_CLIENTE) y devuelva la reserva con estado LIBERADA. Define los mensajes con números de campo correctos, un enum para el motivo, y escribe la implementación del servidor con los códigos gRPC adecuados para: reserva inexistente, reserva ya CONSUMIDA (no se puede liberar), y éxito.
Ejercicio 2. Extiende el esquema GraphQL con type Pago { id: ID!, estado: EstadoPago!, importe: Float!, metodo: String! } y EstadoPago (los estados de 02-03), y añade el campo pago: Pago a Pedido. Escribe el resolver Pedido.pago usando un DataLoader sobre un cliente pagosApi.obtenerPagosPorPedidos(pedidoIds) (GET /pagos?pedidoIds=) y razona por qué el campo debe ser anulable.
Ejercicio 3. Para cada escenario, elige REST, gRPC, GraphQL o mensajería y justifica en dos frases: (a) el almacén físico envía 50.000 movimientos de stock cada noche desde un sistema en Java; (b) un socio externo quiere consultar el estado de sus pedidos desde su ERP; (c) la web de administración interna muestra un cuadro con pedidos del día, sus pagos y las reservas asociadas; (d) al confirmarse un pedido hay que generar la factura en un futuro servicio de Facturación.
Soluciones
Solución 1.
enum MotivoLiberacion {
MOTIVO_LIBERACION_SIN_ESPECIFICAR = 0;
SIN_STOCK = 1;
PAGO_RECHAZADO = 2;
TIMEOUT_PAGO = 3;
CANCELACION_CLIENTE = 4;
}
message SolicitudLiberacion {
string reserva_id = 1;
MotivoLiberacion motivo = 2;
}
service ServicioInventario {
// ... los anteriores ...
rpc LiberarReserva (SolicitudLiberacion) returns (Reserva);
}LiberarReserva: async (llamada, callback) => {
const { reservaId, motivo } = llamada.request;
if (!reservaId || motivo === 'MOTIVO_LIBERACION_SIN_ESPECIFICAR') {
return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'reserva_id y motivo son obligatorios' });
}
const reserva = await repositorioReservas.obtener(reservaId);
if (!reserva) return callback({ code: grpc.status.NOT_FOUND, details: `No existe la reserva ${reservaId}` });
if (reserva.estado === 'CONSUMIDA') {
return callback({ code: grpc.status.FAILED_PRECONDITION, details: 'RESERVA_CONSUMIDA: no se puede liberar' });
}
if (reserva.estado === 'LIBERADA') {
// Idempotente: liberar dos veces devuelve el mismo resultado sin error
return callback(null, aMensaje(reserva));
}
const liberada = await casosDeUso.liberarReserva(reservaId, motivo); // publica stock.liberado
callback(null, aMensaje(liberada));
}Detalle importante: liberar una reserva ya liberada responde OK con la misma reserva, no FAILED_PRECONDITION: la operación es idempotente por diseño, coherente con la reentrega at-least-once de 03-02.
Solución 2.
enum EstadoPago { AUTORIZADO CAPTURADO RECHAZADO REEMBOLSADO }
type Pago { id: ID!, estado: EstadoPago!, importe: Float!, metodo: String! }
extend type Pedido { pago: Pago }function crearCargadorPagos(request) {
const requestId = request.headers.get('x-request-id');
return new DataLoader(async (pedidoIds) => {
const pagos = await pagosApi.obtenerPagosPorPedidos([...pedidoIds], { requestId }); // GET /pagos?pedidoIds=ped-88213,...
const porPedido = new Map(pagos.map(p => [p.pedidoId, p]));
return pedidoIds.map(id => porPedido.get(id) ?? null);
});
}
// en resolvers:
Pedido: { pago: (pedido, _args, ctx) => ctx.cargadorPagos.load(pedido.id) }Anulable por dos motivos: de negocio, un pedido PENDIENTE o STOCK_RESERVADO aún no tiene pago (la saga no ha llegado ahí); y de resiliencia, si Pagos no responde, GraphQL puede devolver el pedido con pago: null y un elemento en errors en lugar de fallar toda la consulta. Un Pago! no nulo propagaría el null hacia arriba y anularía el pedido entero.
Solución 3.
(a) gRPC con streaming de cliente (ImportarStock(stream Movimiento)): volumen alto, cliente en otro lenguaje que genera su código del mismo .proto, binario compacto para 50.000 mensajes. Alternativa válida: un fichero por lotes; pero si se quiere API, gRPC. (b) REST/JSON: socio externo, ERP genérico, necesita curl, OpenAPI y caché; es la API pública por el gateway. (c) GraphQL en un BFF de administración: una pantalla que agrega tres servicios con forma cambiante; con DataLoader sobre GET /pedidos, GET /pagos?pedidoIds= y el contrato de Inventario. Alternativa aceptable: un endpoint de composición REST en el BFF, si el cuadro es estable. (d) Mensajería: Facturación se suscribe a pedido.confirmado en una cola facturacion.pedidos; Pedidos no cambia ni sabe que Facturación existe (03-02, ejercicio 1).
Conclusión
REST/JSON tiene tres carencias claras (verbosidad, contrato débil, forma fija de la respuesta) y hemos visto la herramienta que resuelve cada una. gRPC aporta un contrato .proto del que se genera el código, serialización binaria y HTTP/2 con streaming, y encaja en llamadas internas de alto volumen; hemos escrito el ServicioInventario con ReservarStock y ConsultarDisponibilidad, su servidor y cliente con @grpc/grpc-js y @grpc/proto-loader, y el mapeo de sus códigos de error. GraphQL aporta un esquema tipado del que el cliente pide la forma exacta que necesita, y encaja en la agregación para front-ends; hemos escrito el esquema de pedidos y productos, resolvers con graphql-yoga y la solución al N+1 con DataLoader por petición. La decisión de TechCorp: REST público, eventos entre servicios, gRPC candidato para Pedidos↔Inventario y GraphQL candidato para el BFF móvil, sin adoptar nada hasta que un problema medido lo justifique.
Ese BFF, y el API Gateway del puerto 8080 del que llevamos hablando desde 02-02, son el siguiente tema: qué problemas resuelve tener un único punto de entrada, qué debe hacer (enrutar, autenticar, limitar, agregar) y, sobre todo, qué no debe hacer, cómo se implementa uno en Node.js o se declara en Kong o Traefik, y por qué cada tipo de cliente (web, móvil) merece su propio backend for frontend.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
