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

  1. Limitaciones de REST/JSON que motivan alternativas
  2. gRPC: qué es y cómo funciona
  3. Protocol Buffers: el contrato .proto de Inventario
  4. Servidor y cliente gRPC en Node.js
  5. Errores en gRPC y cuándo usarlo
  6. GraphQL: esquema, consultas y mutaciones
  7. Resolvers en Node.js
  8. El problema N+1 y DataLoader
  9. Cuándo usar GraphQL y sus riesgos
  10. Comparativa: REST, gRPC, GraphQL y mensajería
  11. La decisión de TechCorp

  1. 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).

  1. 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 .proto que 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 int32 ocupa 1-5 bytes; en JSON, "cantidad": 2 ocupa 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.

  1. Protocol Buffers: el contrato .proto de Inventario

En 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. package da un espacio de nombres (y, con v1, deja hueco para versionar, tema de 03-06).
  • service agrupa los rpc. Cada rpc tiene un mensaje de entrada y uno de salida; stream delante de uno de ellos lo convierte en flujo.
  • message es como una struct. Cada campo lleva tipo (string, int32, bool, repeated X, otro message, enum), nombre en snake_case (la convención protobuf; el código generado lo convierte a camelCase en 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 hay null: hay que decidir cómo representar "ausente" (con optional, disponible desde protobuf 3.15, o con un mensaje envolvente).
  • Los enum deben tener un valor 0, que es el defecto; por convenio se llama *_SIN_ESPECIFICAR para 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.

  1. 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.

npm install @grpc/grpc-js @grpc/proto-loader

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.

  1. 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 curl y 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.

  1. 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 }
}

  1. 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í):

npm install graphql graphql-yoga
// 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.

  1. 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.

npm install dataloader
// 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.

  1. 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

  1. 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)

  1. 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 .proto o 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 deadline en el cliente gRPC. Igual que fetch sin signal: 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 errors en la respuesta GraphQL. data puede venir con null parciales 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

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados