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
- Limitacions de REST/JSON que motiven alternatives
- gRPC: què és i com funciona
- Protocol Buffers: el contracte
.protod'Inventari - Servidor i client gRPC en Node.js
- Errors en gRPC i quan fer-lo servir
- GraphQL: esquema, consultes i mutacions
- Resolvers en Node.js
- El problema N+1 i DataLoader
- Quan fer servir GraphQL i els seus riscos
- Comparativa: REST, gRPC, GraphQL i missatgeria
- La decisió de TechCorp
- 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).
- 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
.protoque 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
int32ocupa 1-5 bytes; en JSON,"quantitat": 2n'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.
- Protocol Buffers: el contracte
.proto d'Inventari
.proto d'InventariA 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.packagedona un espai de noms (i, ambv1, deixa lloc per versionar, tema de 03-06).serviceagrupa elsrpc. Cadarpcté un missatge d'entrada i un de sortida;streamdavant d'un d'ells el converteix en flux.messageés com unastruct. Cada camp porta tipus (string,int32,bool,repeated X, un altremessage,enum), nom ensnake_case(la convenció protobuf; el codi generat el converteix acamelCaseen 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 hanull: cal decidir com representar "absent" (amboptional, disponible des de protobuf 3.15, o amb un missatge embolcall). - Els
enumhan de tenir un valor0, que és el per defecte; per conveni s'anomena*_SENSE_ESPECIFICARper 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.
- 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.
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.
- 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
curli 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.
- 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 }
}
- 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í):
// 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.
- 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.
// 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.
- 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 |
- 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 | Sí | Sí | Sí | 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) |
- 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
.protoo 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
deadlineal client gRPC. Igual quefetchsensesignal: 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
errorsa la resposta GraphQL.datapot venir ambnullparcials 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
- Conceptes Bàsics de Microserveis
- Avantatges i Desavantatges dels Microserveis
- Comparació amb l'Arquitectura Monolítica
- Quan Adoptar Microserveis: Criteris de Decisió
- El Cas Pràctic del Curs: la Botiga Online de TechCorp
Mòdul 2: Disseny de Microserveis
- Principis de Disseny de Microserveis
- Descomposició d'Aplicacions Monolítiques
- Definició de Bounded Contexts
- Gestió de Dades: una Base de Dades per Servei
- Consistència Distribuïda: Sagues, CQRS i Event Sourcing
Mòdul 3: Comunicació entre Microserveis
- APIs RESTful
- Missatgeria Asíncrona
- Protocols de Comunicació: gRPC, GraphQL
- API Gateway i Backend for Frontend
- Descobriment de Serveis i Balanceig de Càrrega
- Contractes i Versionat d'APIs
Mòdul 4: Implementació de Microserveis
- Elecció de Tecnologies i Eines
- Desenvolupament d'un Microservei Simple
- Gestió de Configuració
- Integració Pràctica: Consumir APIs i Publicar Esdeveniments
- Proves en Microserveis: Unitàries, d'Integració i de Contracte
Mòdul 5: Desplegament i Orquestració
- Contenidors i Docker
- Orquestració amb Kubernetes
- CI/CD per a Microserveis
- Estratègies de Desplegament: Rolling, Blue-Green i Canary
- Service Mesh: Istio i Linkerd
Mòdul 6: Monitoratge i Manteniment
- Monitoratge i Logging
- Traçabilitat Distribuïda amb OpenTelemetry
- Gestió d'Errors i Recuperació
- Escalabilitat i Rendiment
- SLOs, Alertes i Gestió d'Incidents
Mòdul 7: Seguretat en Microserveis
- Autenticació i Autorització
- Seguretat en la Comunicació
- Pràctiques de Seguretat
- Seguretat en Contenidors i Kubernetes
