TechCorp ja té els seus dos canals principals: REST/JSON per a les crides síncrones (03-01) i RabbitMQ per als esdeveniments (03-02). Amb això es pot construir tot el sistema, i de fet és el que farem al mòdul 4. Però convé conèixer dues alternatives que resolen problemes concrets que REST/JSON resol malament: gRPC, per a crides internes d'alt volum amb contracte tipat i binari, i GraphQL, perquè un front-end demani exactament les dades que necessita de diversos serveis en una sola consulta. No són modes ni substituts de REST: són eines amb un nínxol clar, i saber quin és evita tant ignorar-les com fer-les servir on no toca.

En aquesta lliçó veurem quines limitacions de REST/JSON motiven cada alternativa; gRPC amb Protocol Buffers (un .proto real del servei d'Inventari), HTTP/2, els quatre tipus de crida, i un servidor i un client mínims en Node.js amb @grpc/grpc-js explicats línia a línia; GraphQL amb un esquema de comandes i productes, resolvers, el problema N+1 i DataLoader; una taula comparativa de REST, gRPC, GraphQL i missatgeria; i la decisió de TechCorp sobre on encaixa cadascun. El gateway i el BFF (on GraphQL brillaria) són de 03-04, i el versionat de .proto i esquemes GraphQL, de 03-06.

Contingut

  1. Limitacions de REST/JSON que motiven alternatives
  2. gRPC: què és i com funciona
  3. Protocol Buffers: el contracte .proto d'Inventari
  4. Servidor i client gRPC en Node.js
  5. Errors en gRPC i quan fer-lo servir
  6. GraphQL: esquema, consultes i mutacions
  7. Resolvers en Node.js
  8. El problema N+1 i DataLoader
  9. Quan fer servir GraphQL i els seus riscos
  10. Comparativa: REST, gRPC, GraphQL i missatgeria
  11. La decisió de TechCorp

  1. Limitacions de REST/JSON que motiven alternatives

REST sobre JSON és l'estàndard de facto perquè és simple, universal i llegible. Precisament per això té tres mancances que es noten a mesura que un sistema creix:

Limitació En què consisteix On ho patiria TechCorp
Verbositat i cost de serialització JSON és text: cada camp repeteix el seu nom, els números viatgen com a cadenes de dígits, i parsejar-lo costa CPU. HTTP/1.1 obre connexions i envia capçaleres completes a cada petició. Comandes → Inventari si passés a síncron: milers de crides per minut al pic, cadascuna amb el mateix JSON de línies.
Absència de contracte fort OpenAPI descriu l'API, però és un document a part del codi: res no impedeix que el servidor retorni preu com a cadena un bon dia. Els clients s'escriuen a mà o es generen de manera opcional. Un canvi a Catàleg que trenca el traductorProducte de Comandes es descobreix a producció o, amb sort, a 04-05.
Over-fetching i under-fetching Un endpoint retorna una forma fixa. Si el front necessita menys, sobra (over-fetching); si necessita dades de dos recursos, fa dues peticions (under-fetching). La pantalla de "detall de comanda" de l'app mòbil necessita la comanda, els productes amb les seves imatges i l'estat del pagament: tres crides, o un endpoint a mida per pantalla.

gRPC ataca les dues primeres (binari compacte, HTTP/2 i contracte generat des d'un .proto). GraphQL ataca la tercera (el client declara la forma exacta que vol). Cap no ataca les tres, i cap no resol el que la missatgeria resol (l'acoblament temporal).

  1. gRPC: què és i com funciona

gRPC és un framework de crida a procediment remot (RPC) creat per Google. Els seus ingredients:

  • Contracte primer. S'escriu un fitxer .proto que defineix els serveis, els seus mètodes i els missatges que intercanvien. A partir d'ell es genera el codi de servidor i de client en el llenguatge que sigui (Node.js, Go, Java...). El contracte no és documentació: és la font.
  • Protocol Buffers (protobuf) com a format de serialització: binari, compacte (un int32 ocupa 1-5 bytes; en JSON, "quantitat": 2 n'ocupa 14), i amb esquema (el receptor sap quin tipus té cada camp sense haver d'endevinar).
  • HTTP/2 com a transport: una sola connexió TCP multiplexa moltes crides en paral·lel, les capçaleres es comprimeixen, i hi ha streaming natiu en tots dos sentits.
  • Quatre tipus de crida:
Tipus El client envia El servidor retorna Exemple a TechCorp
Unària 1 missatge 1 missatge ReservarEstoc(SollicitudReserva) → Reserva
Streaming de servidor 1 missatge Flux de N missatges SeguirDisponibilitat(producteId) → stream Disponibilitat (el client rep cada canvi d'estoc)
Streaming de client Flux de N missatges 1 missatge ImportarEstoc(stream Moviment) → Resum (càrrega massiva des del magatzem)
Bidireccional Flux Flux Xat de suport en temps real (fora de l'abast de TechCorp)

La manera mental correcta: gRPC s'assembla a cridar una funció que viu en un altre procés, amb tipus comprovats en compilació (o en càrrega, en Node.js). REST s'assembla a manipular documents. Per això gRPC encaixa en crides internes servei-a-servei i REST en APIs públiques.

  1. Protocol Buffers: el contracte .proto d'Inventari

A 03-01 vam dissenyar POST /reserves com a contracte REST d'Inventari "per si la relació Comandes↔Inventari passa a síncrona". Aquest és el mateix contracte en gRPC:

// inventari.proto
syntax = "proto3";

package techcorp.inventari.v1;

// El servei: cada rpc és un mètode remot
service ServeiInventari {
  // Unària: reservar estoc per a una comanda (equivalent a POST /reserves)
  rpc ReservarEstoc (SollicitudReserva) returns (Reserva);
  // Unària: consultar disponibilitat de diversos productes (lot, com GET /productes?ids=)
  rpc ConsultarDisponibilitat (SollicitudDisponibilitat) returns (RespostaDisponibilitat);
  // Streaming de servidor: rebre canvis de disponibilitat d'un producte
  rpc SeguirDisponibilitat (SollicitudSeguiment) returns (stream Disponibilitat);
}

// Els missatges: cada camp té tipus, nom i NÚMERO. El número és el que viatja per la xarxa;
// el nom és només per al codi. Per això mai no es reutilitza un número (03-06).
message LiniaReserva {
  string producte_id = 1;   // "p-501"
  int32 quantitat = 2;
}

message SollicitudReserva {
  string comanda_id = 1;                // "com-88213"; serveix també de clau d'idempotència
  repeated LiniaReserva linies = 2;     // repeated = llista
  int32 expira_en_segons = 3;           // 900
}

enum EstatReserva {
  ESTAT_RESERVA_SENSE_ESPECIFICAR = 0;  // proto3 exigeix un valor 0 per defecte
  ACTIVA = 1;
  CONSUMIDA = 2;
  ALLIBERADA = 3;
}

message Reserva {
  string id = 1;                        // "res-4471"
  string comanda_id = 2;
  EstatReserva estat = 3;
  string expira_en = 4;                 // ISO 8601; hi ha un tipus Timestamp estàndard, però així és més didàctic
  repeated LiniaReserva linies = 5;
}

message SollicitudDisponibilitat {
  repeated string producte_ids = 1;
}

message Disponibilitat {
  string producte_id = 1;
  int32 unitats_disponibles = 2;
  bool disponible = 3;
}

message RespostaDisponibilitat {
  repeated Disponibilitat productes = 1;
}

message SollicitudSeguiment {
  string producte_id = 1;
}

Com llegir-lo:

  • syntax = "proto3" és la versió actual del llenguatge. package dona un espai de noms (i, amb v1, deixa lloc per versionar, tema de 03-06).
  • service agrupa els rpc. Cada rpc té un missatge d'entrada i un de sortida; stream davant d'un d'ells el converteix en flux.
  • message és com una struct. Cada camp porta tipus (string, int32, bool, repeated X, un altre message, enum), nom en snake_case (la convenció protobuf; el codi generat el converteix a camelCase en JavaScript si se li demana) i un número de camp únic dins del missatge. Aquest número és la identitat del camp en el binari: canviar el nom no trenca res; canviar el número, sí.
  • En proto3 tots els camps són opcionals i tenen valor per defecte ("", 0, false, llista buida). No hi ha null: cal decidir com representar "absent" (amb optional, disponible des de protobuf 3.15, o amb un missatge embolcall).
  • Els enum han de tenir un valor 0, que és el per defecte; per conveni s'anomena *_SENSE_ESPECIFICAR per distingir-lo d'un valor real.

Fixa't que aquest .proto és el contracte entre Comandes i Inventari: si l'equip del Luis i el d'Inventari (que a 02-01 vam veure que són el mateix equip) l'acorden, cadascun genera la seva banda i treballa en paral·lel.

  1. Servidor i client gRPC en Node.js

En Node.js hi ha dues maneres de fer servir un .proto: generar codi estàtic amb protoc i el plugin de JavaScript, o carregar-lo dinàmicament en temps d'execució amb @grpc/proto-loader. La segona és la més senzilla per aprendre i la que farem servir; a producció TechCorp faria servir també la càrrega dinàmica llevat que necessiti tipus TypeScript generats.

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

Servidor (dins de servei-inventari; només la part gRPC, la resta del servei és del mòdul 4):

// grpc/servidorInventari.js
const path = require('node:path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// 1. Carregar i "compilar" el .proto en memòria.
//    keepCase:false converteix producte_id → producteId als objectes JS.
//    longs/enums/defaults controlen com es representen els tipus; aquests valors són els habituals.
const definicio = protoLoader.loadSync(path.join(__dirname, 'inventari.proto'), {
  keepCase: false, longs: String, enums: String, defaults: true, oneofs: true
});
// 2. Convertir la definició en objectes gRPC utilitzables; naveguem fins al paquet
const proto = grpc.loadPackageDefinition(definicio).techcorp.inventari.v1;

// 3. Implementació de cada rpc. La signatura és sempre (crida, callback) per a les unàries.
//    crida.request és el missatge d'entrada ja deserialitzat; callback(error, resposta) respon.
const implementacio = {
  ReservarEstoc: async (crida, callback) => {
    const { comandaId, linies, expiraEnSegons } = crida.request;
    try {
      // La lògica real (transacció sobre estoc/reserves, publicar estoc.reservat) és del mòdul 4
      const reserva = await casosDUs.reservarEstoc({ comandaId, linies, expiraEnSegons });
      callback(null, {
        id: reserva.id, comandaId: reserva.comandaId, estat: 'ACTIVA',
        expiraEn: reserva.expiraEn.toISOString(), linies: reserva.linies
      });
    } catch (err) {
      // 4. Els errors es comuniquen amb codis gRPC, no amb excepcions (apartat 5)
      if (err.codi === 'SENSE_ESTOC') {
        return callback({ code: grpc.status.FAILED_PRECONDITION, details: `SENSE_ESTOC: ${err.message}` });
      }
      callback({ code: grpc.status.INTERNAL, details: 'Error intern' });
    }
  },

  ConsultarDisponibilitat: async (crida, callback) => {
    const { producteIds } = crida.request;
    if (producteIds.length === 0 || producteIds.length > 100) {
      return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'Entre 1 i 100 ids' });
    }
    const files = await repositoriEstoc.disponibilitatDe(producteIds);
    callback(null, {
      productes: files.map(f => ({ producteId: f.producteId, unitatsDisponibles: f.disponibles, disponible: f.disponibles > 0 }))
    });
  },

  // 5. Streaming de servidor: no hi ha callback; s'escriu a 'crida' tants cops com calgui i es tanca amb end()
  SeguirDisponibilitat: (crida) => {
    const { producteId } = crida.request;
    const cancellar = notificadorEstoc.subscriure(producteId, (disp) => {
      crida.write({ producteId, unitatsDisponibles: disp, disponible: disp > 0 });
    });
    crida.on('cancelled', cancellar); // el client ha tallat: deixar d'enviar
  }
};

// 6. Crear el servidor, registrar el servei amb la seva implementació i escoltar.
//    createInsecure() = sense TLS; vàlid dins del clúster per aprendre. TLS/mTLS es veu a 07-02.
function arrencarGrpc(port = 50051) {
  const servidor = new grpc.Server();
  servidor.addService(proto.ServeiInventari.service, implementacio);
  servidor.bindAsync(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure(), (err) => {
    if (err) throw err;
    console.log(`gRPC ServeiInventari escoltant a ${port}`);
  });
  return servidor;
}

module.exports = { arrencarGrpc };

Client (dins de servei-comandes):

// grpc/inventariClient.js
const path = require('node:path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// 1. Mateix .proto, mateixes opcions: el contracte és compartit (en un paquet npm o un repo de contractes)
const definicio = protoLoader.loadSync(path.join(__dirname, 'inventari.proto'), {
  keepCase: false, longs: String, enums: String, defaults: true, oneofs: true
});
const proto = grpc.loadPackageDefinition(definicio).techcorp.inventari.v1;

// 2. Un stub: objecte amb un mètode per rpc. L'adreça és el nom estable del servei (03-05).
const INVENTARI_GRPC = process.env.INVENTARI_GRPC ?? 'servei-inventari:50051';
const stub = new proto.ServeiInventari(INVENTARI_GRPC, grpc.credentials.createInsecure());

// 3. Els stubs fan servir callbacks; els emboliquem en promeses per fer-los servir amb async/await
function reservarEstoc(sollicitud, { requestId } = {}) {
  return new Promise((resolve, reject) => {
    // 4. Metadata = capçaleres gRPC. Propaguem X-Request-Id igual que a REST.
    const metadata = new grpc.Metadata();
    if (requestId) metadata.set('x-request-id', requestId);
    // 5. deadline = timeout absolut: si en 2 s no hi ha resposta, error DEADLINE_EXCEEDED
    const deadline = new Date(Date.now() + 2000);

    stub.ReservarEstoc(sollicitud, metadata, { deadline }, (err, resposta) => {
      if (err) return reject(err);   // err.code és un grpc.status; err.details el text
      resolve(resposta);
    });
  });
}

module.exports = { reservarEstoc };

I el seu ús, amb la comanda de sempre:

try {
  const reserva = await reservarEstoc({
    comandaId: 'com-88213',
    linies: [{ producteId: 'p-501', quantitat: 1 }, { producteId: 'p-777', quantitat: 2 }],
    expiraEnSegons: 900
  }, { requestId: 'req-01J4ZK9X2M' });
  console.log(reserva.id, reserva.estat); // res-4471 ACTIVA
} catch (err) {
  if (err.code === grpc.status.FAILED_PRECONDITION) { /* SENSE_ESTOC → cancel·lar comanda */ }
  else if (err.code === grpc.status.DEADLINE_EXCEEDED) { /* Inventari no respon → 503 */ }
  else throw err;
}

L'essencial: el .proto és l'únic fitxer compartit; servidor i client el carreguen i n'obtenen objectes tipats; els noms de mètode i de camp surten del contracte, no d'una URL escrita a mà; i el deadline és l'equivalent de l'AbortSignal.timeout de 03-01.

  1. Errors en gRPC i quan fer-lo servir

gRPC no fa servir codis HTTP: té els seus, menys nombrosos i més precisos. Els que TechCorp mapa:

Codi gRPC Equivalent REST Quan
OK (0) 200 Èxit
INVALID_ARGUMENT (3) 400 / 422 Petició mal formada o valors invàlids
NOT_FOUND (5) 404 Recurs inexistent
ALREADY_EXISTS (6) 409 Ja existeix (reserva duplicada sense idempotència)
FAILED_PRECONDITION (9) 409 L'estat no permet l'operació: SENSE_ESTOC, transició invàlida
PERMISSION_DENIED (7) / UNAUTHENTICATED (16) 403 / 401 Autorització / autenticació
RESOURCE_EXHAUSTED (8) 429 Límit superat
DEADLINE_EXCEEDED (4) 503/504 a qui crida Timeout
UNAVAILABLE (14) 503 Servidor caigut o arrencant; el client pot reintentar
INTERNAL (13) 500 Fallada no controlada

El codi de negoci (SENSE_ESTOC) viatja a details o, millor, en un missatge d'error estructurat (google.rpc.Status amb detalls tipats); per a TechCorp n'hi ha prou amb el prefix a details i un mapatge al client.

Quan fer servir gRPC:

  • Comunicació interna servei-a-servei amb alt volum o baixa latència exigida: la serialització binària i HTTP/2 es noten a partir de milers de crides per segon.
  • Quan vols un contracte fort i codi generat en diversos llenguatges (equips amb Go, Java i Node.js).
  • Streaming (progrés, seguiment en temps real).

Quan no:

  • APIs públiques consumides per navegadors: els navegadors no parlen gRPC natiu (existeix gRPC-Web amb un proxy, però afegeix peces), i els desenvolupadors externs esperen REST.
  • Quan la llegibilitat importa més que el rendiment: no es pot fer curl i llegir la resposta.
  • Equips petits sense problema de rendiment: és complexitat sense retorn.

Per a TechCorp: Comandes → Inventari és la candidata natural si algun dia es fa síncrona (partnership de 02-03, mateix equip, crides freqüents). Avui va per esdeveniments i continuarà així; el .proto queda dissenyat.

  1. GraphQL: esquema, consultes i mutacions

GraphQL és un llenguatge de consulta per a APIs i un runtime que les executa. La idea central: el servidor publica un esquema tipat de tot el que es pot demanar, i el client envia una consulta que descriu exactament la forma de la resposta que vol. Un sol endpoint (POST /graphql), un sol viatge, ni un camp de més ni de menys.

Esquema (SDL, Schema Definition Language) per a una vista de comandes que combina dades de Comandes i de Catàleg:

# esquema.graphql
type Producte {
  id: ID!
  nom: String!
  preu: Float!
  moneda: String!
  imatgeUrl: String
  disponible: Boolean!
}

type LiniaComanda {
  producteId: ID!
  nom: String!               # congelat a la comanda (02-03)
  quantitat: Int!
  preuUnitari: Float!
  producte: Producte         # dades VIVES de Catàleg! (imatge, disponibilitat actual)
}

enum EstatComanda { PENDENT ESTOC_RESERVAT PAGADA CONFIRMADA CANCELLADA }

type Adreca { carrer: String!, codiPostal: String!, ciutat: String!, pais: String! }

type Comanda {
  id: ID!
  estat: EstatComanda!
  clientId: ID!
  linies: [LiniaComanda!]!
  total: Float!
  adrecaEnviament: Adreca!
  creatEn: String!
}

type Query {
  comanda(id: ID!): Comanda
  comandesDeClient(clientId: ID!, limit: Int = 20, cursor: String): [Comanda!]!
  productes(ids: [ID!]!): [Producte!]!
}

input LiniaEntrada { producteId: ID!, quantitat: Int! }
input AdrecaEntrada { carrer: String!, codiPostal: String!, ciutat: String!, pais: String! }

type Mutation {
  crearComanda(clientId: ID!, linies: [LiniaEntrada!]!, adrecaEnviament: AdrecaEntrada!): Comanda!
}

Com llegir-lo: type defineix objectes amb camps tipats (! = no nul; [X!]! = llista no nul·la d'elements no nuls); Query són les lectures i Mutation les escriptures (totes dues són només tipus especials); input són tipus per a arguments; enum com en protobuf. Fixa't en LiniaComanda.producte: un camp que creua serveis. És el que REST no dona sense un endpoint a mida.

Consulta del client mòbil per a la pantalla de detall de comanda:

query DetallComanda($id: ID!) {
  comanda(id: $id) {
    id
    estat
    total
    linies {
      nom
      quantitat
      preuUnitari
      producte { imatgeUrl disponible }
    }
  }
}

Amb {"id": "com-88213"} com a variables, la resposta té exactament aquesta forma:

{
  "data": {
    "comanda": {
      "id": "com-88213",
      "estat": "CONFIRMADA",
      "total": 79.7,
      "linies": [
        { "nom": "Auriculars BT X200", "quantitat": 1, "preuUnitari": 59.9, "producte": { "imatgeUrl": "https://cdn.techcorp.example/p-501.webp", "disponible": true } },
        { "nom": "Cable USB-C 2 m", "quantitat": 2, "preuUnitari": 9.9, "producte": { "imatgeUrl": "https://cdn.techcorp.example/p-777.webp", "disponible": true } }
      ]
    }
  }
}

Ni clientId, ni adrecaEnviament, ni creatEn: no s'han demanat. I una mutació:

mutation {
  crearComanda(
    clientId: "c-1024",
    linies: [{ producteId: "p-501", quantitat: 1 }, { producteId: "p-777", quantitat: 2 }],
    adrecaEnviament: { carrer: "Gran Vía 12", codiPostal: "28013", ciutat: "Madrid", pais: "ES" }
  ) { id estat }
}

  1. Resolvers en Node.js

L'esquema diu què es pot demanar; els resolvers diuen com s'obté cada camp. Un resolver és una funció (pare, args, context, info) per camp; els que no es defineixen es resolen per defecte llegint la propietat del mateix nom a l'objecte pare. Farem servir graphql-yoga, un servidor GraphQL lleuger que es munta sobre Express (Apollo Server és l'alternativa més coneguda i equivalent per al que fem aquí):

npm install graphql graphql-yoga
// graphql/servidor.js (això viurà al BFF mòbil de 03-04, no a Comandes)
const { createSchema, createYoga } = require('graphql-yoga');
const { readFileSync } = require('node:fs');
const express = require('express');

// Clients REST dels serveis interns (els de 03-01)
const comandesApi = require('../clients/comandesClient');    // GET /comandes/{id}, POST /comandes
const catalegApi = require('../clients/catalegClient');      // GET /productes?ids=

const typeDefs = readFileSync(require.resolve('./esquema.graphql'), 'utf8');

const resolvers = {
  Query: {
    // 1. Resolver arrel: args porta els arguments de la consulta; context, el que injectem per petició
    comanda: (_pare, { id }, context) => comandesApi.obtenirComanda(id, { requestId: context.requestId }),
    productes: (_pare, { ids }, context) => catalegApi.obtenirProductes(ids, { requestId: context.requestId })
  },
  Mutation: {
    crearComanda: (_pare, args, context) =>
      comandesApi.crearComanda(args, { requestId: context.requestId, clauIdempotencia: context.clauIdempotencia })
  },
  LiniaComanda: {
    // 2. Resolver de camp: 'pare' és la línia ja obtinguda pel resolver de dalt.
    //    Només s'executa si la consulta ha demanat 'producte'. AQUÍ neix el problema N+1 (apartat 8).
    producte: (linia, _args, context) => context.carregadorProductes.load(linia.producteId)
  }
};

const yoga = createYoga({
  schema: createSchema({ typeDefs, resolvers }),
  // 3. El context es crea per petició: request id, DataLoader nou, i més endavant l'usuari (07-01)
  context: ({ request }) => ({
    requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
    clauIdempotencia: request.headers.get('idempotency-key'),
    carregadorProductes: crearCarregadorProductes(request) // apartat 8
  }),
  graphqlEndpoint: '/graphql'
});

const app = express();
app.use(yoga.graphqlEndpoint, yoga); // POST /graphql (i GET amb la interfície GraphiQL per provar)

La cadena per a DetallComanda: el motor crida Query.comanda (una crida REST a Comandes), obté la comanda amb les seves dues línies, i per a cada línia crida LiniaComanda.producte. Sense més cura, això són dues crides a Catàleg (una per línia) a més de la de Comandes; amb 20 línies, vint. És l'N+1 que ja vam evitar a REST amb ?ids=; a GraphQL cal evitar-lo amb DataLoader.

  1. El problema N+1 i DataLoader

DataLoader és una petita llibreria (creada per Facebook juntament amb GraphQL) que fa dues coses: agrupa (batching) totes les crides a .load(id) que passen en el mateix tick del bucle d'esdeveniments en una sola crida a la teva funció de lot, i desa en memòria cau per petició per no demanar dues vegades el mateix id.

npm install dataloader
// graphql/carregadors.js
const DataLoader = require('dataloader');
const catalegApi = require('../clients/catalegClient');

function crearCarregadorProductes(request) {
  const requestId = request.headers.get('x-request-id');
  // La funció de lot rep TOTS els ids demanats en aquest tick i ha de retornar
  // un array de la MATEIXA mida i en el MATEIX ordre (null on no existeixi)
  return new DataLoader(async (ids) => {
    const productes = await catalegApi.obtenirProductes([...ids], { requestId }); // UNA crida: GET /productes?ids=p-501,p-777
    const perId = new Map(productes.map(p => [p.id, p]));
    return ids.map(id => perId.get(id) ?? null);
  }, { maxBatchSize: 100 }); // respectem el límit de lot del contracte de Catàleg (03-01)
}

module.exports = { crearCarregadorProductes };

Amb això, la consulta DetallComanda fa exactament dues crides REST (Comandes i Catàleg) sigui quin sigui el nombre de línies. Dues regles: el DataLoader es crea per petició (al context), mai global (la memòria cau filtraria dades entre usuaris i no es refrescaria), i la seva funció de lot ha de respectar l'ordre dels ids.

  1. Quan fer servir GraphQL i els seus riscos

Quan sí:

  • Agregació per a front-ends: una app o web amb moltes pantalles que combinen dades de diversos serveis i evolucionen a ritmes diferents. El client demana el que necessita sense que el back-end creï un endpoint per pantalla. És el cas del BFF que veurem a 03-04: GraphQL és una manera excel·lent d'implementar-lo.
  • Clients amb amplada de banda limitada (mòbil) on l'over-fetching costa.
  • Quan l'equip de front i el de back volen desacoblar el seu ritme: el back publica l'esquema; el front decideix quina consulta fa.

Quan no:

  • Entre serveis interns: els serveis no necessiten flexibilitat de forma; necessiten contractes estables i simples (REST o gRPC) o esdeveniments.
  • Com a façana única de tot el sistema ("un graf per governar-los a tots"): acaba sent un monòlit d'esquema mantingut per un equip coll d'ampolla.

Riscos que cal gestionar des del primer dia:

Risc Per què Mitigació
Consultes costoses El client pot demanar comandesDeClient { linies { producte { ... } } } amb imbricació arbitrària, o llistes enormes Límit de profunditat i de "complexitat" (cada camp suma; es rebutgen consultes per sobre d'un llindar); limit màxim a les llistes; timeouts
Memòria cau HTTP inutilitzada Tot és POST /graphql: els CDN i navegadors no desen en memòria cau per URL Memòria cau al client (Apollo Client, urql), persisted queries (el client envia un hash d'una consulta registrada i es pot fer servir GET), memòria cau per camp al servidor
N+1 Un resolver per camp, ingenu DataLoader sempre que un camp creui serveis
Errors parcials Una consulta pot retornar data amb parts a null i una llista errors; els clients no acostumats ho ignoren Tractar errors sempre; decidir per camp si és anul·lable (Producte a LiniaComanda és anul·lable a propòsit: si Catàleg falla, la comanda es mostra igualment)
Autorització per camp Ja no hi ha "una ruta = un permís" Comprovar permisos als resolvers o amb directives; es veu a 07-01

  1. Comparativa: REST, gRPC, GraphQL i missatgeria

Criteri REST/JSON gRPC GraphQL Missatgeria (RabbitMQ)
Format JSON (text) Protobuf (binari) JSON (text) El que vulguis; TechCorp: JSON
Contracte OpenAPI (opcional, a part del codi) .proto (obligatori, genera codi) Esquema SDL (obligatori, introspectiu) Esquema d'esdeveniments (AsyncAPI, opcional)
Transport HTTP/1.1 o 2 HTTP/2 HTTP (normalment POST) AMQP
Sincronia Síncron Síncron (+ streaming) Síncron (+ subscriptions) Asíncron
Acoblament temporal No
Forma de la resposta Fixa per endpoint Fixa per mètode La decideix el client Fixa per esdeveniment
Llegibilitat / depuració Excel·lent (curl) Baixa (binari; cal grpcurl) Bona (GraphiQL) Mitjana (consola del broker)
Memòria cau HTTP Nativa (ETag, Cache-Control) No Difícil No aplica
Navegadors Natiu Via gRPC-Web + proxy Natiu No (via WebSocket/SSE en un servei)
Rendiment Bo Excel·lent Bo (depèn dels resolvers) Excel·lent per desacoblar i absorbir pics
Casos d'ús APIs públiques, CRUD, crides internes senzilles Intern d'alt volum, políglota, streaming Agregació per a front-ends, BFF Esdeveniments de negoci, sagues, integració
Eines Tot l'ecosistema HTTP protoc, grpcurl, Buf Apollo, Yoga, GraphiQL, DataLoader RabbitMQ, amqplib, consola de gestió
Corba d'aprenentatge Baixa Mitjana Mitjana (alta per fer-ho bé: N+1, complexitat) Mitjana (nou model mental)

  1. La decisió de TechCorp

La Marta i els líders d'equip fixen la política de protocols:

Àmbit Protocol Motiu
API pública (web, app, socis) a través del gateway 8080 REST/JSON amb OpenAPI Universal, es pot desar en memòria cau, depurable; és el que esperen els consumidors externs
Entre serveis, per defecte Esdeveniments a RabbitMQ Sense acoblament temporal; és la saga de 02-05
Entre serveis, quan cal resposta immediata REST/JSON (Comandes → Catàleg, Comandes → Clients) Volum moderat; reutilitza els contractes de 03-01 i les mateixes eines
Comandes ↔ Inventari Esdeveniments avui; gRPC candidat si passa a síncron Mateix equip, partnership, potencial alt volum; el .proto ja està dissenyat
BFF de l'app mòbil GraphQL candidat Pantalles que agreguen Comandes + Catàleg + Pagaments; amplada de banda mòbil; es decideix a 03-04

La regla que resumeix la decisió: no s'introdueix cap protocol nou fins que hi ha un problema mesurat que REST + esdeveniments no resolen. gRPC i GraphQL queden com a eines conegudes i amb un lloc reservat, no com a decisions preses per moda.

Errors Comuns i Consells

  • Adoptar gRPC "perquè és més ràpid" sense mesurar. Si Comandes → Catàleg fa 50 crides per segon, JSON no és el teu problema. Mesura primer (06-04).
  • Reutilitzar números de camp en un .proto o canviar el tipus d'un camp existent. El receptor antic interpretarà bytes amb el significat equivocat. S'aprofundeix a 03-06; de moment: números nous sempre.
  • Oblidar el deadline al client gRPC. Igual que fetch sense signal: la crida pot quedar penjada indefinidament.
  • GraphQL sense DataLoader. L'N+1 a GraphQL és silenciós: en desenvolupament, amb dues línies, no es nota; a producció, amb vint, és una tempesta de peticions a Catàleg.
  • GraphQL sense límits de complexitat. És exposar una consulta arbitrària a Internet. Profunditat màxima i cost màxim des del primer desplegament.
  • DataLoader global (compartit entre peticions). Desa en memòria cau dades d'un usuari per a un altre i no es refresca. Un per petició, al context.
  • Fer servir GraphQL entre serveis interns. Afegeix una capa de resolució i perd la simplicitat de REST/gRPC sense guanyar res: els serveis no necessiten triar la forma.
  • Ignorar errors a la resposta GraphQL. data pot venir amb null parcials i l'error ser en una altra llista. Els clients han de mirar totes dues coses.

Exercicis

Exercici 1. Afegeix al .proto d'Inventari un mètode unari AlliberarReserva que rebi l'id de la reserva i un motiu (SENSE_ESTOC, PAGAMENT_REBUTJAT, TIMEOUT_PAGAMENT, CANCELLACIO_CLIENT) i retorni la reserva amb estat ALLIBERADA. Defineix els missatges amb números de camp correctes, un enum per al motiu, i escriu la implementació del servidor amb els codis gRPC adequats per a: reserva inexistent, reserva ja CONSUMIDA (no es pot alliberar), i èxit.

Exercici 2. Amplia l'esquema GraphQL amb type Pagament { id: ID!, estat: EstatPagament!, import: Float!, metode: String! } i EstatPagament (els estats de 02-03), i afegeix el camp pagament: Pagament a Comanda. Escriu el resolver Comanda.pagament fent servir un DataLoader sobre un client pagamentsApi.obtenirPagamentsPerComandes(comandaIds) (GET /pagaments?comandaIds=) i raona per què el camp ha de ser anul·lable.

Exercici 3. Per a cada escenari, tria REST, gRPC, GraphQL o missatgeria i justifica-ho en dues frases: (a) el magatzem físic envia 50.000 moviments d'estoc cada nit des d'un sistema en Java; (b) un soci extern vol consultar l'estat de les seves comandes des del seu ERP; (c) la web d'administració interna mostra un quadre amb les comandes del dia, els seus pagaments i les reserves associades; (d) en confirmar-se una comanda cal generar la factura en un futur servei de Facturació.

Solucions

Solució 1.

enum MotiuAlliberament {
  MOTIU_ALLIBERAMENT_SENSE_ESPECIFICAR = 0;
  SENSE_ESTOC = 1;
  PAGAMENT_REBUTJAT = 2;
  TIMEOUT_PAGAMENT = 3;
  CANCELLACIO_CLIENT = 4;
}

message SollicitudAlliberament {
  string reserva_id = 1;
  MotiuAlliberament motiu = 2;
}

service ServeiInventari {
  // ... els anteriors ...
  rpc AlliberarReserva (SollicitudAlliberament) returns (Reserva);
}
AlliberarReserva: async (crida, callback) => {
  const { reservaId, motiu } = crida.request;
  if (!reservaId || motiu === 'MOTIU_ALLIBERAMENT_SENSE_ESPECIFICAR') {
    return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'reserva_id i motiu són obligatoris' });
  }
  const reserva = await repositoriReserves.obtenir(reservaId);
  if (!reserva) return callback({ code: grpc.status.NOT_FOUND, details: `No existeix la reserva ${reservaId}` });
  if (reserva.estat === 'CONSUMIDA') {
    return callback({ code: grpc.status.FAILED_PRECONDITION, details: 'RESERVA_CONSUMIDA: no es pot alliberar' });
  }
  if (reserva.estat === 'ALLIBERADA') {
    // Idempotent: alliberar dues vegades retorna el mateix resultat sense error
    return callback(null, aMissatge(reserva));
  }
  const alliberada = await casosDUs.alliberarReserva(reservaId, motiu); // publica estoc.alliberat
  callback(null, aMissatge(alliberada));
}

Detall important: alliberar una reserva ja alliberada respon OK amb la mateixa reserva, no FAILED_PRECONDITION: l'operació és idempotent per disseny, coherent amb el relliurament at-least-once de 03-02.

Solució 2.

enum EstatPagament { AUTORITZAT CAPTURAT REBUTJAT REEMBORSAT }
type Pagament { id: ID!, estat: EstatPagament!, import: Float!, metode: String! }
extend type Comanda { pagament: Pagament }
function crearCarregadorPagaments(request) {
  const requestId = request.headers.get('x-request-id');
  return new DataLoader(async (comandaIds) => {
    const pagaments = await pagamentsApi.obtenirPagamentsPerComandes([...comandaIds], { requestId }); // GET /pagaments?comandaIds=com-88213,...
    const perComanda = new Map(pagaments.map(p => [p.comandaId, p]));
    return comandaIds.map(id => perComanda.get(id) ?? null);
  });
}
// als resolvers:
Comanda: { pagament: (comanda, _args, ctx) => ctx.carregadorPagaments.load(comanda.id) }

Anul·lable per dos motius: de negoci, una comanda PENDENT o ESTOC_RESERVAT encara no té pagament (la saga no hi ha arribat); i de resiliència, si Pagaments no respon, GraphQL pot retornar la comanda amb pagament: null i un element a errors en lloc de fer fallar tota la consulta. Un Pagament! no nul propagaria el null cap amunt i anul·laria la comanda sencera.

Solució 3.

(a) gRPC amb streaming de client (ImportarEstoc(stream Moviment)): volum alt, client en un altre llenguatge que genera el seu codi del mateix .proto, binari compacte per a 50.000 missatges. Alternativa vàlida: un fitxer per lots; però si es vol API, gRPC. (b) REST/JSON: soci extern, ERP genèric, necessita curl, OpenAPI i memòria cau; és l'API pública pel gateway. (c) GraphQL en un BFF d'administració: una pantalla que agrega tres serveis amb forma canviant; amb DataLoader sobre GET /comandes, GET /pagaments?comandaIds= i el contracte d'Inventari. Alternativa acceptable: un endpoint de composició REST al BFF, si el quadre és estable. (d) Missatgeria: Facturació se subscriu a comanda.confirmada en una cua facturacio.comandes; Comandes no canvia ni sap que Facturació existeix (03-02, exercici 1).

Conclusió

REST/JSON té tres mancances clares (verbositat, contracte feble, forma fixa de la resposta) i hem vist l'eina que resol cadascuna. gRPC aporta un contracte .proto del qual es genera el codi, serialització binària i HTTP/2 amb streaming, i encaixa en crides internes d'alt volum; hem escrit el ServeiInventari amb ReservarEstoc i ConsultarDisponibilitat, el seu servidor i client amb @grpc/grpc-js i @grpc/proto-loader, i el mapatge dels seus codis d'error. GraphQL aporta un esquema tipat del qual el client demana la forma exacta que necessita, i encaixa en l'agregació per a front-ends; hem escrit l'esquema de comandes i productes, resolvers amb graphql-yoga i la solució a l'N+1 amb DataLoader per petició. La decisió de TechCorp: REST públic, esdeveniments entre serveis, gRPC candidat per a Comandes↔Inventari i GraphQL candidat per al BFF mòbil, sense adoptar res fins que un problema mesurat ho justifiqui.

Aquest BFF, i l'API Gateway del port 8080 del qual parlem des de 02-02, són el tema següent: quins problemes resol tenir un únic punt d'entrada, què ha de fer (encaminar, autenticar, limitar, agregar) i, sobretot, què no ha de fer, com se n'implementa un en Node.js o es declara a Kong o Traefik, i per què cada tipus de client (web, mòbil) mereix el seu propi backend for frontend.

Curs de Microserveis

Mòdul 1: Introducció als Microserveis

Mòdul 2: Disseny de Microserveis

Mòdul 3: Comunicació entre Microserveis

Mòdul 4: Implementació de Microserveis

Mòdul 5: Desplegament i Orquestració

Mòdul 6: Monitoratge i Manteniment

Mòdul 7: Seguretat en Microserveis

Mòdul 8: Casos d'Estudi i Exemples Pràctics

© Copyright 2026. Tots els drets reservats