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

  1. El problema: molts serveis, un sol client
  2. Responsabilitats transversals del gateway
  3. El que un gateway NO ha de fer
  4. Les rutes del gateway de TechCorp i l'strangler fig
  5. Opcions d'implementació
  6. Un gateway mínim en Express amb http-proxy-middleware
  7. La mateixa configuració a Traefik (YAML declaratiu)
  8. El patró Backend for Frontend
  9. Un BFF mòbil que compon comanda i productes
  10. Riscos del gateway

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

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. L'strangler fig és impossible. Si la web parla directament amb el monòlit, moure /productes al 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.

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

  1. 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 preu a price per 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.

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

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

  1. Un gateway mínim en Express amb http-proxy-middleware

npm install express http-proxy-middleware express-rate-limit cors
// 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 un express.json() global "per costum", http-proxy-middleware deixa de reenviar el cos i tots els POST arriben 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'error del proxy respon en el mateix format RFC 7807 de 03-01, amb el requestId perquè 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.

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

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

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

  1. 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 reenvia POST buits. Si necessites parsejar en alguna ruta pròpia del gateway, fes-ho només en aquella ruta.
  • Rutes en ordre incorrecte. El comodí /api abans que /api/comandes ho captura tot i l'strangler fig deixa de funcionar. Específic primer, comodí últim (o priority a Traefik).
  • Sense timeout al proxy. Un servei penjat esgota les connexions del gateway i tomba l'API sencera. proxyTimeout sempre.
  • 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 amaga Location, ETag i X-Request-Id a la web encara que el servidor les enviï, i el polling després del 202 no 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:

  1. GET /comandes?clientId=c-1024&ordre=-creatEn&limit=10 a Comandes (paginació per cursor de 03-01) → 1 crida.
  2. Recollir el producteId de la primera línia de cada comanda (fins a 10 ids, deduplicats) i fer GET /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

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