Des de 02-02 parlem d'un API Gateway al port 8080 "davant" dels serveis: és la peça que fa possible l'strangler fig (la web continua cridant una sola adreça mentre les rutes es van movent del monòlit als serveis) i l'única porta que veuran la web, l'app mòbil i els socis externs. A 03-03 vam deixar, a més, dues preguntes obertes: on viu el GraphQL que agrega Comandes i Catàleg per a l'app mòbil, i per què parlem d'un backend for frontend. Aquesta lliçó respon a totes dues.
Veurem quins problemes resol un gateway (un punt d'entrada, no exposar els ports 300x, i les responsabilitats transversals: encaminament, autenticació delegada, rate limiting, CORS, terminació TLS, agregació, logging), què no ha de fer, la taula de rutes del gateway de TechCorp i el seu paper a l'strangler fig, les opcions d'implementació, un gateway mínim però complet en Express amb http-proxy-middleware explicat pas a pas, la configuració declarativa equivalent a Traefik, el patró Backend for Frontend amb un BFF mòbil que compon GET /comandes/{id} + GET /productes?ids=, i els riscos del gateway. Com troba el gateway els serveis (descobriment i balanceig) és de 03-05, l'autenticació en detall de 07-01 i el desplegament del gateway del mòdul 5.
Contingut
- El problema: molts serveis, un sol client
- Responsabilitats transversals del gateway
- El que un gateway NO ha de fer
- Les rutes del gateway de TechCorp i l'strangler fig
- Opcions d'implementació
- Un gateway mínim en Express amb
http-proxy-middleware - La mateixa configuració a Traefik (YAML declaratiu)
- El patró Backend for Frontend
- Un BFF mòbil que compon comanda i productes
- Riscos del gateway
- El problema: molts serveis, un sol client
Sense gateway, la web de TechCorp hauria de saber que els productes són a servei-cataleg:3001, les comandes a servei-comandes:3002 i els clients a servei-clients:3004. Això porta cinc problemes immediats:
- Acoblament del client a la topologia. Cada vegada que un servei canvia de host, es divideix o es fusiona, cal redesplegar la web i publicar una versió nova de l'app mòbil (i esperar que els usuaris l'actualitzin).
- Superfície d'atac. Exposar sis ports a Internet és exposar sis superfícies que cal autenticar, apedaçar i vigilar. Inventari i Pagaments, a més, no han de ser accessibles des de fora sota cap concepte.
- Duplicació del que és transversal. Autenticació, CORS, límits de peticions, TLS, registre d'accessos: o ho fa cada servei (sis vegades, amb sis versions lleugerament diferents) o ho fa algú davant de tots.
- Molts viatges de xarxa. Una pantalla que necessita dades de tres serveis fa tres peticions des del mòbil, cadascuna amb la seva latència i el seu handshake.
- L'strangler fig és impossible. Si la web parla directament amb el monòlit, moure
/productesal servei nou requereix canviar la web. Amb un gateway, es canvia una regla d'encaminament i ningú més no se n'assabenta.
Un API Gateway és un servidor que rep totes les peticions externes i les reenvia al servei intern adequat, aplicant pel camí les polítiques comunes. És un reverse proxy amb criteri.
flowchart LR
W[Web] --> G
M[App mòbil] --> G
S[Socis / ERP] --> G
G[API Gateway :8080]
G -- "/api/productes/*" --> C[servei-cataleg:3001]
G -- "/api/comandes/*" --> P[servei-comandes:3002]
G -- "/api/clients/*" --> K[servei-clients:3004]
G -- "resta (encara)" --> MO[monòlit techcorp-shop:3000]
I[servei-inventari:3006]:::intern
PA[servei-pagaments:3003]:::intern
N[servei-notificacions:3005]:::intern
classDef intern stroke-dasharray: 5 5
Els serveis amb línia discontínua no tenen ruta al gateway: només parlen per esdeveniments (03-02) o reben crides internes.
- Responsabilitats transversals del gateway
| Responsabilitat | Què fa el gateway | Per què aquí i no a cada servei |
|---|---|---|
| Encaminament | Mapa rutes públiques a serveis interns (/api/comandes/* → servei-comandes:3002), reescrivint el prefix |
És la seva raó de ser; permet l'strangler fig |
| Autenticació delegada | Valida el JWT (signatura, caducitat, emissor Keycloak) i rebutja amb 401 abans de tocar cap servei; propaga la identitat en capçaleres o el mateix token |
Un sol lloc on estar al dia de claus i algorismes; els serveis reben peticions ja autenticades (tot i que a 07-01 veurem que també han de verificar) |
| Rate limiting | Limita peticions per IP, per client o per token (429 + Retry-After) |
Protegeix tots els serveis de cop; un servei saturat no es pot protegir a si mateix |
| CORS | Respon a les peticions OPTIONS de preflight i afegeix Access-Control-Allow-* |
El navegador només veu un origen (el gateway); configurar CORS a sis serveis és garantia d'inconsistència |
| Terminació TLS | Rep HTTPS de l'exterior i parla HTTP (o mTLS amb el mesh de 05-05) cap endins | Un sol certificat que renovar; els serveis no gestionen claus privades |
| Agregació | Compon diverses respostes internes en una (amb matisos: apartat 8) | Estalvia viatges al client mòbil |
| Logging i mètriques d'accés | Registra mètode, ruta, codi, latència i X-Request-Id de cada petició |
Vista única del trànsit entrant; entrada a 06-01 |
| Correlació | Genera X-Request-Id si no ve i el propaga |
És el primer salt: si no ho fa ell, ningú més no pot |
| Timeouts | Talla peticions que un servei no respon en N segons (504) |
Evita que un servei lent pengi connexions del client |
| Transformació lleugera | Reescriure rutes, afegir/treure capçaleres, comprimir | Adaptació entre el que és públic i el que és intern |
- El que un gateway NO ha de fer
A 02-01 vam formular "smart endpoints, dumb pipes": la intel·ligència viu als serveis; les canonades (broker, gateway) només mouen missatges. Aplicat al gateway:
- Gens de lògica de negoci. El gateway no calcula totals, no valida que una comanda tingui línies, no decideix si un client pot comprar. Si ho fa, es converteix en un minimonòlit que tots els equips han de tocar per a qualsevol canvi, i en el pitjor coll d'ampolla organitzatiu.
- No accedeix a bases de dades dels serveis. Ni per a "una consulta ràpida".
- No transforma càrregues de negoci (canviar el nom de
preuapriceper a un client). Això és un BFF (apartat 8) o una versió d'API (03-06). - No orquestra sagues. El flux de comanda viu als serveis i a RabbitMQ.
- No substitueix la seguretat de cada servei. Que el gateway autentiqui no eximeix Comandes de comprovar que la comanda és de qui la demana (07-01: defensa en profunditat).
La prova del cotó: si per canviar una regla de negoci cal redesplegar el gateway, la regla és al lloc equivocat.
- Les rutes del gateway de TechCorp i l'strangler fig
Taula de rutes en l'estat actual del pla (Catàleg, Comandes i Clients ja extrets o en extracció):
| Ruta pública (gateway :8080) | Destí intern | Reescriptura | Autenticació | Notes |
|---|---|---|---|---|
GET /api/productes, GET /api/productes/{id} |
servei-cataleg:3001 |
/api/productes → /productes |
Pública (lectura anònima) | Primera ruta migrada (02-02); memòria cau de 30 s permesa |
POST /api/comandes, GET /api/comandes, GET /api/comandes/{id}, POST /api/comandes/{id}/cancellacio |
servei-comandes:3002 |
/api/comandes → /comandes |
JWT obligatori | Requereix Idempotency-Key al POST |
GET /api/clients/{id}, PUT /api/clients/{id}/adreca |
servei-clients:3004 |
/api/clients → /clients |
JWT; només el mateix client o admin | |
/api/graphql (mòbil) |
bff-mobil:3010 |
cap | JWT | Apartat 9 |
/api/* (tota la resta) |
monòlit techcorp-shop:3000 |
cap | segons el monòlit | Rutes encara no migrades: es van buidant |
/health |
El mateix gateway | — | Pública | Per al balancejador de davant |
| (sense ruta) | servei-inventari:3006, servei-pagaments:3003, servei-notificacions:3005 |
— | — | No exposats. S'hi arriba només per esdeveniments o des d'altres serveis |
L'strangler fig es veu a l'última fila de /api/*: al principi del projecte tot anava al monòlit. L'equip de Plataforma va afegir la regla de /api/productes/* quan Catàleg va estar llest; afegirà /api/comandes/* quan ho estigui Comandes, i així fins que la regla del monòlit no rebi trànsit i s'esborri. Cada pas és un canvi de configuració del gateway, reversible en segons: si el Catàleg nou falla, es torna a apuntar /api/productes/* al monòlit.
Dues convencions: les rutes públiques porten el prefix /api/ per distingir-les dels recursos estàtics de la web, i el gateway treu aquest prefix abans de reenviar, de manera que els serveis exposen /productes, /comandes, exactament com els contractes de 03-01, sense saber que hi ha un gateway al davant.
- Opcions d'implementació
| Opció | Què és | Avantatges | Inconvenients | Encaix a TechCorp |
|---|---|---|---|---|
| Kong | Gateway sobre NGINX/OpenResty amb plugins (auth, rate limit, transformacions) i configuració declarativa o per API | Molt complet, ecosistema de plugins, mode declaratiu sense BD | Una peça més a operar; corba mitjana | Bona opció a mitjà termini |
| NGINX | Reverse proxy clàssic configurat amb nginx.conf |
Ubic, rapidíssim, estable | Configuració estàtica, sense descobriment natiu; rate limit i auth requereixen mòduls | Senzill però rígid per a l'strangler fig |
| Traefik | Reverse proxy natiu de contenidors; descobreix serveis des de Docker/Kubernetes i es configura amb YAML o etiquetes | Integració natural amb Docker Compose i Kubernetes (Ingress), middlewares de rate limit, headers, auth | Menys plugins que Kong | Elecció de TechCorp per a producció |
| Spring Cloud Gateway | Gateway programable en Java/Spring | Molt integrat a l'ecosistema Spring | TechCorp no fa servir Java | Només menció |
Gateway propi en Node.js (http-proxy-middleware o express-http-proxy) |
Un Express que fa de proxy | Control total, mateix llenguatge que la resta, ideal per aprendre i per a lògica de composició | Cal implementar i mantenir el que Kong/Traefik donen fet; risc de ficar-hi negoci | Per a aquesta lliçó i per als BFF |
L'estratègia de TechCorp: entendre el gateway construint-ne un de mínim en Node.js (apartat 6), operar a producció un de declaratiu (Traefik, apartat 7; a Kubernetes farà d'Ingress, 05-02), i programar només els BFF, que sí que tenen lògica de composició legítima.
- Un gateway mínim en Express amb
http-proxy-middleware
http-proxy-middleware// gateway/servidor.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const { randomUUID } = require('node:crypto');
const app = express();
// 1. Destins interns. Venen de configuració (04-03); els noms estables es justifiquen a 03-05.
const DESTINS = {
cataleg: process.env.CATALEG_URL ?? 'http://servei-cataleg:3001',
comandes: process.env.COMANDES_URL ?? 'http://servei-comandes:3002',
clients: process.env.CLIENTS_URL ?? 'http://servei-clients:3004',
bffMobil: process.env.BFF_MOBIL_URL ?? 'http://bff-mobil:3010',
monolit: process.env.MONOLIT_URL ?? 'http://techcorp-shop:3000'
};
// 2. Correlació: si el client no porta X-Request-Id, el generem aquí. Tot el que passi
// després (logs del gateway, capçalera al servei, resposta al client) el porta.
app.use((req, res, next) => {
req.requestId = req.get('X-Request-Id') ?? `req-${randomUUID()}`;
res.set('X-Request-Id', req.requestId);
next();
});
// 3. CORS: només els orígens de TechCorp. El navegador fa OPTIONS (preflight) i aquest
// middleware respon; els serveis interns no saben res de CORS.
app.use(cors({
origin: ['https://shop.techcorp.example', 'https://admin.techcorp.example'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'Idempotency-Key', 'If-Match', 'X-Request-Id'],
exposedHeaders: ['Location', 'ETag', 'X-Request-Id', 'Retry-After'],
maxAge: 600
}));
// 4. Rate limiting: 300 peticions per minut per IP a tota l'API. Amb diverses rèpliques del
// gateway caldria un magatzem compartit (Redis); en memòria n'hi ha prou per aprendre.
app.use('/api', rateLimit({
windowMs: 60_000,
limit: 300,
standardHeaders: 'draft-7', // afegeix RateLimit-* i Retry-After
legacyHeaders: false,
message: { type: 'about:blank', title: 'Massa peticions', status: 429, codi: 'LIMIT_PETICIONS' }
}));
// 5. Log d'accés mínim (06-01 el substituirà per logs estructurats)
app.use((req, res, next) => {
const inici = Date.now();
res.on('finish', () => {
console.log(JSON.stringify({ requestId: req.requestId, metode: req.method, ruta: req.originalUrl, estat: res.statusCode, ms: Date.now() - inici }));
});
next();
});
// 6. Autenticació delegada (esquelet). La validació real del JWT de Keycloak es veu a 07-01;
// aquí només comprovem que la capçalera existeix per a les rutes protegides.
function requereixToken(req, res, next) {
const auth = req.get('Authorization') ?? '';
if (!auth.startsWith('Bearer ')) {
return res.status(401).type('application/problem+json').json({ type: 'about:blank', title: 'No autenticat', status: 401, codi: 'NO_AUTENTICAT' });
}
next();
}
// 7. Fàbrica de proxies: mateixa configuració per a tots, canvien el destí i la reescriptura
function proxyCapA(desti, { treurePrefix }) {
return createProxyMiddleware({
target: desti,
changeOrigin: true, // posa el Host del destí a la petició reenviada
pathRewrite: treurePrefix ? { [`^${treurePrefix}`]: '' } : undefined, // /api/comandes/x → /comandes/x
proxyTimeout: 5_000, // si el servei no respon en 5 s → 504 al client
timeout: 10_000, // temps màxim de la connexió entrant
on: {
proxyReq: (proxyReq, req) => {
proxyReq.setHeader('X-Request-Id', req.requestId); // propagar correlació
proxyReq.setHeader('X-Forwarded-Prefix', treurePrefix ?? ''); // el servei pot construir Location absoluts si vol
},
error: (err, req, res) => {
// El servei no hi és o ha tancat la connexió: 503 en format RFC 7807, sense filtrar detalls
if (!res.headersSent) {
res.status(503).type('application/problem+json').json({
type: 'about:blank', title: 'Servei no disponible', status: 503,
codi: 'DEPENDENCIA_NO_DISPONIBLE', detail: `Torna-ho a provar més tard (ref ${req.requestId})`
});
}
}
}
});
}
// 8. Taula de rutes. L'ORDRE importa: Express avalua de dalt a baix, i l'última és el comodí cap al monòlit.
app.use('/api/productes', proxyCapA(DESTINS.cataleg, { treurePrefix: '/api' }));
app.use('/api/comandes', requereixToken, proxyCapA(DESTINS.comandes, { treurePrefix: '/api' }));
app.use('/api/clients', requereixToken, proxyCapA(DESTINS.clients, { treurePrefix: '/api' }));
app.use('/api/graphql', requereixToken, proxyCapA(DESTINS.bffMobil, { treurePrefix: '/api' }));
app.use('/api', proxyCapA(DESTINS.monolit, { treurePrefix: null })); // strangler fig: el que no s'ha migrat
// 9. Salut del mateix gateway (03-05 explica liveness/readiness)
app.get('/health', (_req, res) => res.json({ estat: 'ok' }));
app.listen(8080, () => console.log('API Gateway escoltant a 8080'));Explicació de les decisions:
- No hi ha
express.json(). El gateway no necessita parsejar els cossos: els reenvia tal qual com a stream. Parsejar-los costaria CPU i trencaria la transmissió de cossos grans. Quan en un projecte real s'afegeix unexpress.json()global "per costum",http-proxy-middlewaredeixa de reenviar el cos i tots elsPOSTarriben buits: és l'error clàssic. pathRewriteés la traducció entre el contracte públic (/api/comandes) i l'intern (/comandes). Els serveis no saben que hi ha prefix.proxyTimeout: 5000és la bona pràctica mínima de què parlem a cada lliçó; sense ell, un Catàleg penjat reté connexions del gateway fins a esgotar-les. Els reintents i el circuit breaker són de 06-03.- L'ordre de les rutes implementa l'strangler fig: el que és específic primer, el comodí cap al monòlit al final. Migrar una ruta és afegir una línia damunt del comodí.
requereixTokenés un esquelet: a 07-01 verificarà signatura, caducitat i audience del JWT de Keycloak i passarà la identitat al servei.- L'
errordel proxy respon en el mateix format RFC 7807 de 03-01, amb elrequestIdperquè suport el pugui cercar als logs.
Prova manual de la comanda de sempre a través del gateway (amb Comandes escoltant al 3002):
curl -i -X POST http://localhost:8080/api/comandes \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3c9a2e-1b4d-4e8f-9c21-5a6b7c8d9e0f" \
-d '{"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"}}'Hauria de retornar 202 Accepted, Location: /comandes/com-88213 (el servei construeix la ruta interna; el gateway la podria reescriure a /api/comandes/... amb un onProxyRes, o el client hi afegeix el prefix; TechCorp opta perquè el client conegui el prefix /api), i X-Request-Id.
- La mateixa configuració a Traefik (YAML declaratiu)
A producció, mantenir un gateway a mà no compensa: Traefik fa el mateix amb YAML. El seu model té tres conceptes: routers (quines peticions capturo: regles de host i ruta), middlewares (què els faig: treure prefix, limitar, capçaleres) i services (on les envio, amb balanceig). Configuració dinàmica equivalent al gateway anterior:
# traefik/dinamica.yml
http:
routers:
productes:
rule: "PathPrefix(`/api/productes`)"
entryPoints: [web]
middlewares: [treure-api, limit-global, request-id]
service: cataleg
comandes:
rule: "PathPrefix(`/api/comandes`)"
entryPoints: [web]
middlewares: [treure-api, limit-global, request-id, requereix-token]
service: comandes
clients:
rule: "PathPrefix(`/api/clients`)"
entryPoints: [web]
middlewares: [treure-api, limit-global, request-id, requereix-token]
service: clients
monolit:
rule: "PathPrefix(`/api`)"
entryPoints: [web]
priority: 1 # la més baixa: només si cap de les anteriors encaixa (strangler fig)
middlewares: [limit-global, request-id]
service: monolit
middlewares:
treure-api:
stripPrefix:
prefixes: ["/api"]
limit-global:
rateLimit:
average: 300 # peticions per minut (period)
period: 1m
burst: 50
request-id:
headers:
customRequestHeaders:
X-Forwarded-Prefix: "/api" # Traefik ja afegeix X-Request-Id si s'activa l'accessLog amb aquell camp; aquí només el prefix
requereix-token:
forwardAuth: # delega la validació en un servei d'auth (07-01)
address: "http://auth-gateway:3020/verificar"
authResponseHeaders: ["X-Usuari-Id", "X-Rols"]
services:
cataleg:
loadBalancer:
servers:
- url: "http://servei-cataleg:3001"
healthCheck: { path: /health/ready, interval: 10s }
comandes:
loadBalancer:
servers:
- url: "http://servei-comandes:3002"
clients:
loadBalancer:
servers:
- url: "http://servei-clients:3004"
monolit:
loadBalancer:
servers:
- url: "http://techcorp-shop:3000"I la configuració estàtica mínima (ports, i a producció TLS amb Let's Encrypt, la part de certificats de la qual es veu a 07-02):
# traefik/traefik.yml
entryPoints:
web:
address: ":8080"
providers:
file:
filename: /etc/traefik/dinamica.yml
watch: true # recarrega en canviar el fitxer: migrar una ruta no requereix reinici
accessLog: {}Correspondència amb el codi d'Express: cada app.use('/api/x', ...) és un router + stripPrefix; express-rate-limit és el middleware rateLimit; requereixToken és forwardAuth; el comodí cap al monòlit és el router amb priority: 1; i DESTINS són els services. Quan a 05-02 despleguem a Kubernetes, aquests services apuntaran als Service del clúster i Traefik els descobrirà tot sol (03-05 explica com).
- El patró Backend for Frontend
Un gateway serveix tots els clients per igual. Però la web d'escriptori, l'app mòbil i l'ERP d'un soci necessiten coses diferents: l'app vol respostes petites i compostes (una pantalla, una petició); la web tolera diverses crides i vol memòria cau; el soci vol REST estable i documentat. Si el gateway intenta acontentar tothom amb transformacions i agregacions, s'engreixa (risc de l'apartat 10).
El patró Backend for Frontend (BFF) proposa un servei de façana per tipus de client, propietat de l'equip que fa aquell client: bff-mobil el manté l'equip de l'app; bff-web, el de la web. Cada BFF compon i adapta les APIs dels serveis a les necessitats del seu front, i només d'ell.
| API Gateway | BFF | |
|---|---|---|
| Quants | Un (o un per zona) | Un per tipus de client |
| Coneix el negoci | No | Sí, el de la presentació: què necessita cada pantalla |
| Conté lògica | Transversal (auth, límits) | De composició i adaptació (agregar, filtrar, donar forma) |
| Qui el manté | Plataforma | L'equip del front corresponent |
| On se situa | Davant de tot | Darrere del gateway, davant dels serveis |
| Canvia quan | Canvia la topologia o una política | Canvia una pantalla |
A TechCorp: el gateway encamina /api/graphql al bff-mobil:3010 (taula de l'apartat 4). La web, de moment, consumeix els serveis REST directament a través del gateway; si les seves pantalles es compliquen, tindrà el seu bff-web. L'ERP dels socis fa servir l'API REST pública sense BFF.
- Un BFF mòbil que compon comanda i productes
La pantalla "detall de comanda" de l'app necessita la comanda (Comandes) i la imatge i disponibilitat actual de cada producte (Catàleg). A 03-03 ho vam resoldre amb GraphQL + DataLoader, i aquell codi és exactament el BFF mòbil. Aquí mostrem l'alternativa REST, més senzilla, per deixar clara la idea de composició; totes dues són vàlides i TechCorp provarà primer la REST i passarà a GraphQL quan les pantalles ho demanin.
// bff-mobil/rutes/comandes.js
const express = require('express');
const enrutador = express.Router();
const comandesApi = require('../clients/comandesClient'); // GET /comandes/{id} (03-01)
const catalegApi = require('../clients/catalegClient'); // GET /productes?ids= (03-01)
// GET /mobil/comandes/:id → una sola resposta amb comanda + dades vives de producte
enrutador.get('/mobil/comandes/:id', async (req, res, next) => {
const ctx = { requestId: req.get('X-Request-Id') };
try {
// 1. Primer la comanda: sense ella no sabem quins productes demanar
const comanda = await comandesApi.obtenirComanda(req.params.id, ctx);
// 2. Després, UNA crida en lot a Catàleg (mai N+1)
const ids = comanda.linies.map(l => l.producteId);
let productes = [];
try {
productes = await catalegApi.obtenirProductes(ids, ctx);
} catch (err) {
// 3. Degradació elegant: si Catàleg falla, la pantalla es mostra sense imatges.
// El BFF ho decideix perquè coneix la pantalla; el gateway mai no podria.
console.warn('Catàleg no disponible; resposta sense dades vives', { requestId: ctx.requestId });
}
const perId = new Map(productes.map(p => [p.id, p]));
// 4. Donar forma per a la pantalla: només el que l'app pinta, amb noms pensats per al front
res.set('Cache-Control', 'no-store').json({
id: comanda.id,
estat: comanda.estat,
estatLlegible: ESTATS_LLEGIBLES[comanda.estat] ?? comanda.estat, // "Confirmada"
total: comanda.total,
potCancellar: Boolean(comanda._links?.cancellar), // fa servir l'HATEOAS de 03-01
linies: comanda.linies.map(l => ({
nom: l.nom,
quantitat: l.quantitat,
preuUnitari: l.preuUnitari,
imatgeUrl: perId.get(l.producteId)?.imatgeUrl ?? null,
disponibleAra: perId.get(l.producteId)?.disponible ?? null
}))
});
} catch (err) {
if (err.codi === 'COMANDA_NO_EXISTEIX') return res.status(404).json({ codi: err.codi });
next(err);
}
});
const ESTATS_LLEGIBLES = {
PENDENT: 'Processant la teva comanda', ESTOC_RESERVAT: 'Estoc reservat', PAGADA: 'Pagament rebut',
CONFIRMADA: 'Confirmada', CANCELLADA: 'Cancel·lada'
};
module.exports = enrutador;Què fa legítim aquest codi en un BFF i no al gateway: sap quina pantalla el crida, decideix què degradar si una dependència falla, tradueix estats a textos i camps a noms de front. Tot això és coneixement de presentació, no de negoci (el BFF no canvia l'estat de la comanda ni recalcula totals), i és de l'equip de l'app. Quan l'app tingui cinc pantalles així, l'esquema GraphQL de 03-03 substituirà cinc rutes de composició per un sol endpoint flexible.
- Riscos del gateway
| Risc | Símptoma | Mitigació |
|---|---|---|
| Punt únic de fallada | Cau el gateway, cau tota l'API encara que els sis serveis estiguin sans | Diverses rèpliques darrere d'un balancejador (03-05); gateway sense estat (el rate limit a Redis, no en memòria); /health vigilat |
| Coll d'ampolla | Tota petició hi passa: si és lent, tot és lent; si satura la CPU, tot espera | Que faci poc (proxy, no parseig de cossos); escalat horitzontal; mesurar la latència que afegeix (06-04) |
| Gateway "gras" | Regles de negoci, transformacions per client, crides a BD; tots els equips toquen el seu repositori | La regla de l'apartat 3; BFFs per a l'específic de cada client; revisió de canvis per Plataforma |
| Falsa sensació de seguretat | "El gateway ja autentica" i els serveis accepten qualsevol petició interna | Defensa en profunditat (07-01, 07-02): els serveis verifiquen identitat; el mesh xifra el trànsit intern |
| Acoblament al gateway | Els serveis construeixen URLs pensant en /api/ o depenen de capçaleres del gateway |
Els serveis exposen els seus contractes "nets"; el que és del gateway es queda al gateway |
| Configuració com a codi no versionat | Es canvia una regla a mà a producció i ningú no sap què hi ha | El YAML de Traefik al repositori, desplegat per CI/CD (05-03) |
Errors Comuns i Consells
express.json()global al gateway. Consumeix l'stream del cos i el proxy reenviaPOSTbuits. Si necessites parsejar en alguna ruta pròpia del gateway, fes-ho només en aquella ruta.- Rutes en ordre incorrecte. El comodí
/apiabans que/api/comandesho captura tot i l'strangler fig deixa de funcionar. Específic primer, comodí últim (oprioritya Traefik). - Sense timeout al proxy. Un servei penjat esgota les connexions del gateway i tomba l'API sencera.
proxyTimeoutsempre. - CORS "a tot arreu". Amb
origin: '*'i credencials, el navegador ho rebutja i a més obres l'API a qualsevol web. Llista blanca d'orígens. - Rate limit en memòria amb diverses rèpliques. Cada rèplica compta pel seu compte i el límit real és N vegades més gran. Magatzem compartit quan hi hagi més d'una instància.
- Exposar Inventari i Pagaments "per depurar" amb una ruta temporal. Les rutes temporals es queden. Per depurar,
kubectl port-forward(05-02) o el panell d'administració intern. - Ficar l'agregació al gateway "perquè només és una pantalla". La segona pantalla arriba en una setmana. BFF des de la primera.
- Un BFF compartit entre web i mòbil. Deixa de ser "for frontend": torna a ser un gateway gras amb un altre nom. Un per tipus de client.
- Oblidar exposar capçaleres a CORS (
exposedHeaders). El navegador amagaLocation,ETagiX-Request-Ida la web encara que el servidor les enviï, i el polling després del202no troba la URL.
Exercicis
Exercici 1. L'equip de Comandes ha acabat l'extracció i /api/comandes/* ja apunta al servei nou. Ara toca /api/clients/*, però la Marta demana un desplegament prudent: durant una setmana, només les peticions amb la capçalera X-Canary: clients han d'anar al servei-clients:3004; la resta, al monòlit. Escriu la modificació del gateway Express (una funció router d'http-proxy-middleware o un middleware previ) i explica com ho revertiries en segons. Esmenta quina lliçó del curs tracta aquest tipus de desplegament en general.
Exercici 2. L'app mòbil necessita una pantalla "les meves últimes comandes" que mostri, per a cadascuna de les últimes 10 comandes del client, el seu estat, total i la imatge del primer producte. Dissenya la ruta del BFF mòbil (GET /mobil/les-meves-comandes), indica quines crides internes fa (amb els contractes de 03-01), quantes són en total amb i sense lot, i escriu la resposta JSON d'exemple amb dues comandes.
Exercici 3. Un desenvolupador proposa afegir al gateway una comprovació: "si el POST /api/comandes porta més de 50 línies, rebutjar amb 422 abans d'arribar a Comandes, així protegim el servei". Argumenta si és responsabilitat del gateway, del servei de Comandes, o de tots dos, i quina versió d'aquesta idea sí que seria acceptable al gateway.
Solucions
Solució 1.
http-proxy-middleware accepta a router una funció que retorna el destí per petició:
app.use('/api/clients', requereixToken, createProxyMiddleware({
target: DESTINS.monolit, // destí per defecte
changeOrigin: true,
proxyTimeout: 5_000,
router: (req) => req.get('X-Canary') === 'clients' ? DESTINS.clients : DESTINS.monolit,
pathRewrite: (path, req) => req.get('X-Canary') === 'clients' ? path.replace(/^\/api/, '') : path,
on: { proxyReq: (proxyReq, req) => proxyReq.setHeader('X-Request-Id', req.requestId) }
}));Cal declarar-la abans del comodí /api. Reversió: canviar router perquè retorni sempre DESTINS.monolit (o treure la ruta i deixar que el comodí l'absorbeixi) i redesplegar el gateway, o, a Traefik, editar el router amb watch: true sense reinici. Quan la setmana acabi bé, s'elimina la condició i /api/clients va sempre al servei nou. Això és un desplegament canary dirigit per capçalera; les estratègies de desplegament (rolling, blue-green, canary per percentatge de trànsit) es tracten a 05-04.
Solució 2.
Ruta: GET /mobil/les-meves-comandes (el client surt del JWT; a 07-01 el gateway o el BFF l'extreuen). Crides internes:
GET /comandes?clientId=c-1024&ordre=-creatEn&limit=10a Comandes (paginació per cursor de 03-01) → 1 crida.- Recollir el
producteIdde la primera línia de cada comanda (fins a 10 ids, deduplicats) i ferGET /productes?ids=p-501,p-777,...a Catàleg → 1 crida.
Total: 2 crides amb lot; 11 sense lot (1 + una per comanda). Resposta:
{
"comandes": [
{ "id": "com-88213", "estat": "CONFIRMADA", "estatLlegible": "Confirmada", "total": 79.70, "creatEn": "2026-08-15T10:32:07Z", "imatgeUrl": "https://cdn.techcorp.example/p-501.webp", "numLinies": 2 },
{ "id": "com-88102", "estat": "CONFIRMADA", "estatLlegible": "Confirmada", "total": 24.50, "creatEn": "2026-08-14T18:05:44Z", "imatgeUrl": "https://cdn.techcorp.example/p-310.webp", "numLinies": 1 }
],
"seguentCursor": "eyJjcmVhdEVuIjo..."
}Si Catàleg falla, imatgeUrl va a null i la llista es mostra igualment (degradació decidida pel BFF).
Solució 3.
La regla "màxim 50 línies per comanda" és una regla de negoci de l'agregat Comanda (02-03): la decideix i l'aplica el servei de Comandes, que respon 422 DADES_NO_VALIDES segons el seu contracte (03-01). Si viu al gateway, el dia que el negoci canviï el límit a 100 caldrà redesplegar el gateway, i l'equip del Luis dependrà de Plataforma per a una regla seva; a més, un POST /reserves intern o el panell d'administració, que no passen pel gateway, no l'aplicarien. És exactament el gateway "gras" de l'apartat 10.
El que sí que és acceptable al gateway és una protecció transversal sense semàntica de negoci: un límit de mida de cos (per exemple, 256 KB per a qualsevol POST, amb 413 Payload Too Large) que protegeix tots els serveis de cossos gegants sense saber què és una "línia de comanda". El servei continua validant les 50 línies; el gateway només evita que li arribin 50 MB.
Conclusió
L'API Gateway del port 8080 és l'única porta de TechCorp: encamina les rutes públiques /api/productes, /api/comandes i /api/clients cap als seus serveis (i la resta, cada vegada menys, cap al monòlit, que és l'strangler fig en acció), i concentra el que és transversal: correlació amb X-Request-Id, CORS, rate limiting, autenticació delegada, timeouts i registre d'accessos. L'hem construït en Express amb http-proxy-middleware per entendre'l i l'hem declarat a Traefik per operar-lo. I hem separat amb claredat el que no li correspon (lògica de negoci, agregacions a mida) i a qui sí: els backends for frontend, un per tipus de client, mantinguts per l'equip del front, on encaixen tant la composició REST del bff-mobil com el GraphQL de 03-03. Inventari, Pagaments i Notificacions no tenen ruta pública: només parlen per esdeveniments.
Ara bé, en tot el codi d'aquesta lliçó hem escrit destins com http://servei-cataleg:3001 com si fossin adreces fixes. No ho són: a producció hi haurà vuit rèpliques de Catàleg als pics, cadascuna amb una IP efímera que Kubernetes assigna i destrueix. A quina d'elles crida el gateway? I Comandes, quan consulta el catàleg? Com sap algú quines instàncies són vives i quines no? Això és el descobriment de serveis i el balanceig de càrrega, la lliçó següent.
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
