A 03-01 vam triar Express i no vam discutir la decisió: calia començar per algun lloc i Express és el punt de partida més didàctic de l'ecosistema Node, perquè no amaga res. Cada middleware que vam registrar a src/app.js el vam escriure nosaltres, i per això entenem què fa cadascun.

Però Express no és l'única opció, ni tan sols dins de JavaScript, i la pregunta «quin framework fem servir?» apareix el primer dia de qualsevol projecte nou. Es respon gairebé sempre malament: per costum, per moda o per un benchmark llegit a mitges.

Aquesta lliçó dona la perspectiva que falta. Implementarem el mateix endpoint, GET /v1/cafes, en nou frameworks diferents de cinc llenguatges, amb el mateix contracte: els filtres de 02-06, la paginació obligatòria, la resposta {dades, total} i la validació que produeix el 400 del catàleg. Compararem què fa el framework per tu i què et deixa a tu, i acabarem amb els criteris honestos d'elecció —els que no surten als gràfics de peticions per segon— i amb l'observació més important del mòdul: gairebé res del que has après en aquest curs depèn del framework.

Aquesta lliçó no és un tutorial d'instal·lació. No muntaràs nou projectes. Els fragments hi són per ser llegits i comparats, no executats; cadascun assumeix el projecte ja creat amb les eines del llenguatge corresponent.

Contingut

  1. Per què existeix aquesta lliçó
  2. Què s'espera avui d'un framework d'API
  3. L'endpoint de referència
  4. Express: el mínim, tot a càrrec teu
  5. Fastify: esquemes, plugins i velocitat
  6. NestJS: arquitectura opinada per a equips grans
  7. Hono: lleuger i multi-runtime
  8. FastAPI (Python): el tipatge com a contracte
  9. Django REST Framework (Python): serializers i viewsets
  10. Spring Boot (Java): l'estàndard empresarial
  11. ASP.NET Core (C#): minimal APIs i rendiment
  12. Mencions: Laravel, Rails API i Go
  13. La taula comparativa
  14. Criteris d'elecció honestos
  15. El que no depèn del framework
  16. Migrar entre frameworks: d'Express a Fastify
  17. Runtimes alternatius i serverless

  1. Per què existeix aquesta lliçó

Tres situacions concretes fan que aquesta comparació importi:

  • Comences un projecte i has de triar. La decisió condiciona els anys següents: contractacions, formació, dependències i velocitat de lliurament. Es pren en una reunió d'una hora i es paga durant cinc anys.
  • Canvies de feina i el projecte és en un altre framework. Si entens quin problema resol cadascun, aterres en dies en lloc de en mesos.
  • Algú proposa migrar. Necessites arguments millors que «Fastify és més ràpid» per decidir si compensa.

I hi ha un quart motiu, més de fons: veure el mateix endpoint nou vegades ensenya què és essencial en una API REST —el contracte, els codis, la validació, la paginació— i què és accidental, propi del framework de torn. És la millor vacuna contra confondre Express amb REST.

  1. Què s'espera avui d'un framework d'API

Un framework d'API es jutja per quant d'aquesta llista et dona resolt i amb quina qualitat:

Capacitat Què significa Com ho vam resoldre a Express
Encaminament Associar mètode + ruta a una funció Router d'Express
Middleware Cadena de funcions abans i després del manejador 16 posicions a src/app.js
Validació d'entrada Rebutjar dades invàlides abans de la lògica Zod + middleware/validacio.js (03-04)
Serialització de sortida Convertir objectes de domini a JSON del contracte Mapejadors a mà (03-03)
Injecció de dependències Que les capes no s'instanciïn entre si Imports directes i repositoris/index.js
Documentació automàtica Produir OpenAPI sense escriure'l a part A mà (02-08, 05-02)
Gestió d'errors Un sol punt que tradueix excepcions a HTTP middleware/errors.js (03-07)
Rendiment Peticions per segon i latència sota càrrega Suficient; mesurat amb autocannon (04-06)
Tipatge Que el compilador detecti errors de contracte Cap: JavaScript sense tipus
Ecosistema Que existeixi un plugin per al que necessites Enorme
Maduresa i suport Que continuï viu d'aquí a cinc anys Màxima

Observa quantes caselles de la columna dreta diuen «a mà». Això no és un defecte d'Express: és la seva proposta. La pregunta d'aquesta lliçó és què guanyes i què perds quan un altre framework omple aquelles caselles per tu.

  1. L'endpoint de referència

El contracte que implementaran els nou, tal com el vam fixar a 02-06 i 05-02:

GET /v1/cafes?torrefaccio=clar&preuMax=15&limit=20&desplacament=0&ordenar=-preuEuros
Authorization: Bearer <jwt>
{
  "dades": [
    {
      "id": "caf_001",
      "nom": "Etiòpia Yirgacheffe",
      "origen": "Etiòpia",
      "torrefaccio": "clar",
      "preuEuros": 14.50,
      "estoc": 120
    }
  ],
  "total": 137
}

Regles que cada implementació ha de complir:

  1. limit per defecte 20, màxim 100; desplacament màxim 10.000.
  2. torrefaccio només admet clar, mitja o fosc.
  3. Un paràmetre invàlid produeix 400 amb {"error": {"codi": "parametre_invalid", ...}}.
  4. El preu s'emmagatzema en cèntims enters i se serialitza en euros amb dos decimals.
  5. Requereix autenticació.

Aquella quarta regla és la més reveladora: és on es veu si el framework serialitza per tu i si et deixa controlar la transformació.

  1. Express: el mínim, tot a càrrec teu

El nostre punt de partida, condensat per poder-lo comparar:

// src/rutes/cafes.js
import { Router } from 'express';
import { autenticar } from '../middleware/autenticacio.js';
import { validar } from '../middleware/validacio.js';
import { asincron } from '../middleware/asincron.js';
import { esquemaConsultaCafes } from '../esquemes/cafes.js';
import { obtenirCafes } from '../controladors/cafes.js';

export const rutesCafes = Router();

rutesCafes.get(
  '/',
  autenticar,                                   // 03-06
  validar(esquemaConsultaCafes, 'query'),       // 03-04
  asincron(obtenirCafes),                       // 03-07: captura promeses rebutjades
);
// src/esquemes/cafes.js — el contracte d'entrada, en Zod
import { z } from 'zod';

export const esquemaConsultaCafes = z.object({
  torrefaccio: z.enum(['clar', 'mitja', 'fosc']).optional(),
  origen: z.string().min(2).max(60).optional(),
  preuMin: z.coerce.number().min(0).optional(),
  preuMax: z.coerce.number().min(0).optional(),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  desplacament: z.coerce.number().int().min(0).max(10000).default(0),
  ordenar: z.string().default('nom'),
}).strict();   // .strict(): un paràmetre desconegut produeix 400
// src/controladors/cafes.js
import { serveiCafes } from '../serveis/cafes.js';
import { cafeARepresentacio } from '../serveis/mapejadors.js';

export async function obtenirCafes(req, res) {
  const { dades, total } = await serveiCafes.llistar(req.validat.query);
  // El mapejador converteix cèntims a euros: la conversió és EXPLÍCITA i nostra
  res.json({ dades: dades.map(cafeARepresentacio), total });
}

Balanç d'Express. El codi és transparent: es llegeix de dalt a baix i no hi ha màgia. Cada garantia del contracte existeix perquè la vam escriure. El cost és que la llista de «a mà» de l'apartat 2 és llarga, i que res no t'obliga: és perfectament possible que un company registri una ruta sense validar i ningú no se n'assabenti fins que arribi un 500.

  1. Fastify: esquemes, plugins i velocitat

Fastify va néixer preguntant-se si es podia tenir la simplicitat d'Express amb millor rendiment. La resposta va ser que sí, i el mecanisme és interessant: JSON Schema com a peça central.

// rutes/cafes.js — Fastify
// L'esquema NO és només validació: també genera la documentació
// i compila un serialitzador específic, que és d'on surt la velocitat.
const esquemaLlistarCafes = {
  tags: ['Cafès'],
  summary: 'Llista el catàleg de cafès',
  operationId: 'obtenirCafes',
  security: [{ bearerJWT: [] }],
  querystring: {
    type: 'object',
    additionalProperties: false,          // paràmetre desconegut → 400 automàtic
    properties: {
      torrefaccio: { type: 'string', enum: ['clar', 'mitja', 'fosc'] },
      origen: { type: 'string', minLength: 2, maxLength: 60 },
      preuMin: { type: 'number', minimum: 0 },
      preuMax: { type: 'number', minimum: 0 },
      limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
      desplacament: { type: 'integer', minimum: 0, maximum: 10000, default: 0 },
      ordenar: { type: 'string', default: 'nom' },
    },
  },
  response: {
    200: {
      type: 'object',
      properties: {
        dades: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: { type: 'string' },
              nom: { type: 'string' },
              origen: { type: 'string' },
              torrefaccio: { type: 'string' },
              preuEuros: { type: 'number' },
              estoc: { type: 'integer' },
            },
          },
        },
        total: { type: 'integer' },
      },
    },
    400: { $ref: 'Error#' },
  },
};

export default async function rutesCafes(fastify) {
  fastify.get(
    '/cafes',
    { schema: esquemaLlistarCafes, preHandler: [fastify.autenticar] },
    async (peticio) => {
      // peticio.query ja està validada I amb els valors per defecte aplicats
      const { dades, total } = await fastify.serveis.cafes.llistar(peticio.query);
      // No cal res.json(): retornar l'objecte n'hi ha prou.
      return { dades: dades.map(cafeARepresentacio), total };
    },
  );
}

Tres coses que Fastify fa diferent i que convé entendre:

  • La validació és declarativa i automàtica. No hi ha middleware validar: l'esquema querystring el fa el framework, i produeix el 400 sol. Guanyes garantia —no te'n pots oblidar— i perds control sobre el format de l'error, que s'ha de personalitzar amb setErrorHandler perquè encaixi amb el nostre catàleg.
  • L'esquema de resposta compila un serialitzador. Fastify converteix aquell response.200 en una funció de serialització especialitzada, més ràpida que JSON.stringify genèric. Efecte secundari crucial: els camps no declarats a l'esquema s'eliminen de la resposta. És una protecció esplèndida contra fuites accidentals de dades (04-02) i, alhora, la causa número u de «he afegit un camp i no apareix».
  • Els plugins tenen encapsulació real. Un plugin registrat en un àmbit no contamina els altres, a diferència d'app.use a Express, que és global. Això permet, per exemple, aplicar un rate limiting diferent a /v1/sessions sense trucs.

La documentació és gairebé gratis, perquè els esquemes ja estan escrits:

// servidor.js — Fastify genera OpenAPI a partir dels esquemes de les rutes
await fastify.register(import('@fastify/swagger'), {
  openapi: {
    info: { title: 'API de la Botiga Aroma', version: '1.7.0' },
    servers: [{ url: 'https://api.botigaaroma.example/v1' }],
  },
});
await fastify.register(import('@fastify/swagger-ui'), { routePrefix: '/docs' });

Aquí hi ha la diferència pràctica amb 05-02: a Express vam escriure openapi.yaml a mà i arrisquem deriva; a Fastify l'esquema és la validació i és la documentació. A canvi, l'especificació resultant és més pobra en descripcions i exemples si ningú no els escriu.

  1. NestJS: arquitectura opinada per a equips grans

NestJS és el framework més opinat de l'ecosistema Node. Porta TypeScript, decoradors, mòduls i injecció de dependències; el seu model mental ve d'Angular i, més enrere, de Spring.

// cafes/dto/consulta-cafes.dto.ts — el contracte d'entrada com a CLASSE
import { IsOptional, IsIn, IsInt, IsNumber, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';

export class ConsultaCafesDto {
  @ApiPropertyOptional({ enum: ['clar', 'mitja', 'fosc'] })
  @IsOptional()
  @IsIn(['clar', 'mitja', 'fosc'])
  torrefaccio?: 'clar' | 'mitja' | 'fosc';

  @ApiPropertyOptional({ minimum: 0 })
  @IsOptional()
  @Type(() => Number)          // la query arriba com a string: cal convertir-la
  @IsNumber()
  @Min(0)
  preuMax?: number;

  @ApiPropertyOptional({ default: 20, maximum: 100 })
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  @Max(100)
  limit: number = 20;

  @ApiPropertyOptional({ default: 0, maximum: 10000 })
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(0)
  @Max(10000)
  desplacament: number = 0;
}
// cafes/cafes.controller.ts
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOkResponse, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CafesService } from './cafes.service';
import { ColleccioCafesDto } from './dto/colleccio-cafes.dto';

@ApiTags('Cafès')
@ApiBearerAuth()
@Controller('cafes')                       // el prefix /v1 es posa globalment
@UseGuards(JwtAuthGuard)                   // autenticació per a tot el controlador
export class CafesController {
  // Injecció de dependències per constructor: Nest resol CafesService sol.
  // Això és el que fa trivial substituir-lo per un doble a les proves.
  constructor(private readonly cafesService: CafesService) {}

  @Get()
  @ApiOkResponse({ type: ColleccioCafesDto })
  async obtenirCafes(@Query() consulta: ConsultaCafesDto): Promise<ColleccioCafesDto> {
    // consulta ja està validada i transformada pel ValidationPipe global.
    return this.cafesService.llistar(consulta);
  }
}
// cafes/cafes.module.ts — el mòdul declara què fa servir i què exposa
import { Module } from '@nestjs/common';
import { CafesController } from './cafes.controller';
import { CafesService } from './cafes.service';
import { CafesRepositori } from './cafes.repositori';

@Module({
  controllers: [CafesController],
  providers: [
    CafesService,
    // El repositori s'injecta per token: canviar SQLite per PostgreSQL
    // o per un doble a les proves és canviar aquesta línia, res més.
    { provide: 'REPOSITORI_CAFES', useClass: CafesRepositori },
  ],
  exports: [CafesService],
})
export class CafesModule {}

Què hi guanyes. Estructura idèntica a tots els projectes i equips, cosa que redueix a dies el temps d'incorporació d'algú nou. Injecció de dependències de debò, que fa trivials les proves amb dobles —el que a 03-08 vam aconseguir a mà amb el repositori en memòria—. Documentació OpenAPI generada des dels DTO amb @nestjs/swagger. I una convenció tan forta que les discussions sobre estructura de carpetes desapareixen.

Què hi pagues. Molt codi per a poc: el DTO anterior són trenta línies per al que en Zod són sis. Corba d'aprenentatge real —mòduls, proveïdors, àmbits, guàrdies, interceptors, canonades—. Arrencada i consum de memòria més grans. I una capa d'abstracció que, quan falla, obliga a entendre'n les interioritats.

Quan compensa. Equips de més de cinc persones, projectes de llarg recorregut, dominis complexos amb molts mòduls. Per a una API de sis endpoints és un vestit massa gran.

  1. Hono: lleuger i multi-runtime

Hono respon a una pregunta nova: i si la teva API no s'executa en un servidor Node, sinó a la vora de la xarxa —Cloudflare Workers, Deno Deploy, Bun—?

// rutes/cafes.ts — Hono
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { autenticar } from '../middleware/autenticacio';

const esquemaConsulta = z.object({
  torrefaccio: z.enum(['clar', 'mitja', 'fosc']).optional(),
  preuMax: z.coerce.number().min(0).optional(),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  desplacament: z.coerce.number().int().min(0).max(10000).default(0),
});

export const rutesCafes = new Hono();

rutesCafes.get(
  '/cafes',
  autenticar,
  zValidator('query', esquemaConsulta, (resultat, c) => {
    // Manejador d'error propi: així el 400 encaixa amb el NOSTRE catàleg
    if (!resultat.success) {
      return c.json({
        error: {
          codi: 'parametre_invalid',
          missatge: 'Paràmetres de consulta invàlids.',
          detalls: resultat.error.issues.map((i) => ({
            camp: i.path.join('.'),
            problema: i.message,
          })),
        },
      }, 400);
    }
  }),
  async (c) => {
    const consulta = c.req.valid('query');            // tipat, sense assercions
    const { dades, total } = await llistarCafes(c.env.BD, consulta);
    return c.json({ dades: dades.map(cafeARepresentacio), total });
  },
);

L'interessant de Hono no és la sintaxi —molt semblant a Express— sinó que fa servir APIs web estàndard: Request, Response, fetch. El mateix codi s'executa a Node, Deno, Bun, Cloudflare Workers i AWS Lambda. És diminut (uns pocs kilobytes), cosa que importa molt en entorns on l'arrencada en fred es mesura en mil·lisegons.

El seu límit és l'altra cara de la mateixa moneda: a la vora no tens sistema de fitxers ni connexions TCP persistents, així que better-sqlite3 i el pool de PostgreSQL de 03-05 no existeixen; cal fer servir serveis de dades amb API HTTP. I el seu ecosistema és molt menor que el d'Express.

  1. FastAPI (Python): el tipatge com a contracte

FastAPI és probablement la millor demostració d'una idea: si el llenguatge té anotacions de tipus, el framework en pot deduir la validació, la serialització i la documentació.

# rutes/cafes.py — FastAPI
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field

router = APIRouter(prefix="/cafes", tags=["Cafès"])


class Cafe(BaseModel):
    """Un cafè del catàleg, tal com s'exposa a l'API."""
    id: str = Field(pattern=r"^caf_[A-Za-z0-9]+$", examples=["caf_001"])
    nom: str = Field(max_length=120)
    origen: str
    torrefaccio: Literal["clar", "mitja", "fosc"]
    preu_euros: float = Field(serialization_alias="preuEuros", ge=0)
    estoc: int = Field(ge=0)


class ColleccioCafes(BaseModel):
    dades: list[Cafe]
    total: int = Field(description="Total d'elements que compleixen el filtre.")


@router.get(
    "",
    response_model=ColleccioCafes,
    operation_id="obtenirCafes",
    summary="Llista el catàleg de cafès",
    responses={400: {"description": "Paràmetre de consulta invàlid."}},
)
async def obtenir_cafes(
    # Cada paràmetre és un argument tipat: FastAPI el valida i el documenta.
    torrefaccio: Annotated[Literal["clar", "mitja", "fosc"] | None, Query()] = None,
    preu_max: Annotated[float | None, Query(ge=0, alias="preuMax")] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    desplacament: Annotated[int, Query(ge=0, le=10_000)] = 0,
    ordenar: Annotated[str, Query()] = "nom",
    # Dependència injectada: valida el JWT i retorna l'usuari, o llança 401.
    usuari: Annotated[Usuari, Depends(usuari_actual)] = None,
) -> ColleccioCafes:
    dades, total = await servei_cafes.llistar(
        torrefaccio=torrefaccio, preu_max=preu_max,
        limit=limit, desplacament=desplacament, ordenar=ordenar,
    )
    return ColleccioCafes(dades=dades, total=total)

El que passa amb aquestes vint línies, sense escriure res més:

  • Validació completa de tipus i rangs, amb 422 automàtic (personalitzable al nostre 400).
  • Conversió de tipus: limit=20 arriba com a text i es lliura com a int.
  • Documentació OpenAPI 3.1 completa, servida a /docs amb Swagger UI i a /redoc amb Redoc, sense una línia extra.
  • Serialització amb els àlies preu_eurospreuEuros, que resol l'etern xoc entre el snake_case de Python i el camelCase del JSON.
  • Injecció de dependències amb Depends, que a més fa trivial substituir usuari_actual a les proves.

FastAPI és la resposta més elegant del panorama a la deriva entre codi i contracte de 05-02. Els seus límits: Python és més lent que els runtimes compilats —encara que FastAPI, sobre async, és dels més ràpids de l'ecosistema—, i l'async de Python és fàcil d'espatllar: una crida bloquejant dins d'una funció async congela tot el bucle d'esdeveniments, un error tan clàssic com l'await oblidat a Node.

  1. Django REST Framework (Python): serializers i viewsets

DRF és l'altra escola: no un framework d'API, sinó una capa d'API sobre un framework web complet. La seva premissa és que ja tens models de Django i els vols exposar.

# cafes/serializers.py
from rest_framework import serializers
from .models import Cafe


class CafeSerializer(serializers.ModelSerializer):
    """Converteix el model Cafe a la representació pública i a la inversa."""
    # El model desa cèntims enters; el contracte exposa euros.
    preuEuros = serializers.SerializerMethodField()
    notesTast = serializers.ListField(source="notes_tast", child=serializers.CharField())

    class Meta:
        model = Cafe
        fields = ["id", "nom", "origen", "torrefaccio", "preuEuros", "estoc", "notesTast"]
        read_only_fields = ["id"]

    def get_preuEuros(self, obj) -> float:
        return obj.preu_centims / 100
# cafes/views.py
from rest_framework import viewsets, permissions
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter
from .models import Cafe
from .serializers import CafeSerializer
from .pagination import PaginacioAroma


class CafeViewSet(viewsets.ModelViewSet):
    """
    Un ViewSet genera de cop list, retrieve, create, update i destroy.
    És la màxima expressió de la "convenció sobre configuració" de Django.
    """
    queryset = Cafe.objects.all()
    serializer_class = CafeSerializer
    permission_classes = [permissions.IsAuthenticated]
    pagination_class = PaginacioAroma           # produeix {"dades": [...], "total": n}
    filter_backends = [DjangoFilterBackend, OrderingFilter]
    filterset_fields = {
        "torrefaccio": ["exact", "in"],
        "origen": ["exact", "icontains"],
        "preu_centims": ["gte", "lte"],
    }
    ordering_fields = ["nom", "preu_centims", "estoc"]
    ordering = ["nom"]
# cafes/urls.py — el router genera totes les rutes del recurs
from rest_framework.routers import DefaultRouter
from .views import CafeViewSet

router = DefaultRouter()
router.register(r"cafes", CafeViewSet, basename="cafe")
urlpatterns = router.urls

Quaranta línies produeixen els set endpoints del recurs complet, amb filtratge, ordenació, paginació, permisos i una consola HTML navegable. És, amb diferència, la productivitat inicial més alta de tot aquest recorregut.

El preu. Els ViewSets exposen la forma del teu model de dades, no la del teu contracte d'API, i aquí xoca de ple amb 02-02: el dia que el contracte hagi de divergir del model —camps calculats, agregacions, noms diferents, subrecursos de transició d'estat com /comandes/{id}/pagament— lluites contra el framework. Es pot fer, amb serializers i accions personalitzades, però cada excepció costa més que si ho haguessis escrit a mà. A més, Django arrossega una filosofia completa: el seu ORM, el seu sistema de migracions, el seu admin. Si no els faràs servir, DRF és un pes mort.

Quan compensa: un CRUD sobre un model relacional que ja existeix, amb panell d'administració inclòs. És imbatible en aquell terreny.

  1. Spring Boot (Java): l'estàndard empresarial

Spring Boot és el framework que més APIs corporatives sosté al món. El seu model —anotacions, injecció de dependències, capes— és el que NestJS imita.

// CafeControlador.java
package example.botigaaroma.cafes;

import jakarta.validation.constraints.*;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController
@RequestMapping("/v1/cafes")
@Tag(name = "Cafès", description = "Catàleg de cafès d'especialitat")
public class CafeControlador {

    private final CafeServei cafeServei;

    // Injecció per constructor: Spring resol la dependència en arrencar.
    public CafeControlador(CafeServei cafeServei) {
        this.cafeServei = cafeServei;
    }

    @GetMapping
    @PreAuthorize("isAuthenticated()")
    @Operation(operationId = "obtenirCafes", summary = "Llista el catàleg de cafès")
    public ResponseEntity<ColleccioCafes> obtenirCafes(
            @RequestParam(required = false)
            @Pattern(regexp = "clar|mitja|fosc", message = "torrefaccio invàlida")
            String torrefaccio,

            @RequestParam(required = false) @DecimalMin("0") BigDecimal preuMax,

            @RequestParam(defaultValue = "20") @Min(1) @Max(100) int limit,

            @RequestParam(defaultValue = "0") @Min(0) @Max(10000) int desplacament,

            @RequestParam(defaultValue = "nom") String ordenar) {

        var filtre = new FiltreCafes(torrefaccio, preuMax, ordenar);
        var pagina = cafeServei.llistar(filtre, limit, desplacament);

        return ResponseEntity.ok()
                .eTag(pagina.etag())                       // 04-06
                .header("Link", pagina.capcaleraLink())    // 02-06
                .body(new ColleccioCafes(pagina.dades(), pagina.total()));
    }
}
// ColleccioCafes.java — un record: immutable i concís
public record ColleccioCafes(List<CafeDto> dades, long total) {}

// CafeDto.java — la representació pública, separada de l'entitat JPA
public record CafeDto(
        String id,
        String nom,
        String origen,
        String torrefaccio,
        BigDecimal preuEuros,   // BigDecimal, MAI double, per als diners
        int estoc) {}

Java resol netament una cosa que en JavaScript és un problema real: BigDecimal per als diners. En JavaScript, 0.1 + 0.2 no és 0.3, i per això a 02-05 vam decidir desar cèntims enters. Java té un tipus decimal exacte de sèrie, igual que C# amb decimal.

El seu ecosistema és l'argument més gran: Spring Security per a OAuth i OIDC (04-03), Spring Data per a persistència, Actuator que dona /salut i mètriques de Prometheus (04-07) pràcticament gratis, springdoc-openapi per a la documentació. Tot el que al mòdul 4 vam construir peça a peça existeix aquí com a dependència estàndard, revisada i amb suport comercial.

El preu: verbositat, arrencada lenta (segons, encara que GraalVM ho mitiga), consum de memòria alt, i una corba d'aprenentatge llarga on el problema no és Java sinó la quantitat de conceptes de Spring.

  1. ASP.NET Core (C#): minimal APIs i rendiment

ASP.NET Core és, als benchmarks públics, un dels frameworks web més ràpids que existeixen, i el seu mode minimal API elimina bona part de la cerimònia clàssica de C#.

// Program.cs — minimal API completa
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<ICafeServei, CafeServei>();        // injecció de dependències
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();                   // metadades per a OpenAPI
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.MapGet("/v1/cafes", async (
        [AsParameters] ConsultaCafes consulta,
        ICafeServei servei) =>
    {
        // La validació de rangs es fa amb un filtre d'endpoint o amb
        // una biblioteca com FluentValidation; C# no la porta de sèrie.
        if (consulta.Limit is < 1 or > 100)
        {
            return Results.BadRequest(new ErrorApi(
                "parametre_invalid", "El paràmetre 'limit' ha d'estar entre 1 i 100."));
        }

        var (dades, total) = await servei.LlistarAsync(consulta);
        return Results.Ok(new ColleccioCafes(dades, total));
    })
    .RequireAuthorization()
    .WithName("obtenirCafes")
    .WithTags("Cafès")
    .WithOpenApi();

app.Run();

// Tipus del contracte: records immutables, amb decimal per als diners
record ConsultaCafes(string? Torrefaccio, decimal? PreuMax, int Limit = 20,
                     int Desplacament = 0, string Ordenar = "nom");

record CafeDto(string Id, string Nom, string Origen, string Torrefaccio,
               decimal PreuEuros, int Estoc);

record ColleccioCafes(IReadOnlyList<CafeDto> Dades, long Total);

record ErrorApi(string Codi, string Missatge, object[]? Detalls = null);

Punts destacables: [AsParameters] agrupa els paràmetres de consulta en un record tipat, decimal dona aritmètica exacta per als diners, i el rendiment amb el mateix maquinari sol estar entre els millors del panorama. El seu punt fluix relatiu és la validació declarativa, que no ve de sèrie amb la potència de Pydantic o Zod.

Quan triar-lo: organitzacions ja a l'ecosistema Microsoft, o quan el rendiment per servidor és un factor de cost real. I convé desmuntar el prejudici: ASP.NET Core és multiplataforma, de codi obert i s'executa a Linux i en contenidors amb normalitat.

  1. Mencions: Laravel, Rails API i Go

Laravel (PHP). Continua sent dominant a la web PHP i el seu mode API amb Eloquent, API Resources i Sanctum és productiu i ben documentat. El seu allotjament és el més barat i disponible del món. Estigmatitzat sense motiu: el PHP modern amb tipus poc té a veure amb el de fa quinze anys.

Ruby on Rails API (rails new --api). Pare de la «convenció sobre configuració» que va inspirar mig sector. Enorme productivitat inicial, ideal per a prototips i startups. El seu rendiment per procés és modest i l'ecosistema ha perdut impuls davant d'altres.

Go amb net/http (que des de Go 1.22 encamina amb patrons de mètode i ruta), o amb Gin o Echo:

// manejadors/cafes.go — Go amb la biblioteca estàndard
func ObtenirCafes(servei *ServeiCafes) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		consulta, err := parsejarConsultaCafes(r.URL.Query())
		if err != nil {
			// La gestió explícita d'errors és la marca de la casa a Go:
			// verbosa, però cap error no passa desapercebut.
			respondreError(w, http.StatusBadRequest, "parametre_invalid", err.Error())
			return
		}

		dades, total, err := servei.Llistar(r.Context(), consulta)
		if err != nil {
			respondreError(w, http.StatusInternalServerError, "error_intern", "")
			return
		}

		respondreJSON(w, http.StatusOK, ColleccioCafes{Dades: dades, Total: total})
	}
}

Go destaca en tres coses: binari únic sense runtime (imatges Docker de pocs megues, arrencada instantània), concurrència amb goroutines i molt poca memòria per petició, i una biblioteca estàndard tan completa que molts equips no fan servir framework. A canvi, escrius més codi: no hi ha validació declarativa ni generació automàtica d'OpenAPI sense eines addicionals.

  1. La taula comparativa

Framework Llenguatge Corba Opinat Validació OpenAPI automàtic Rendiment relatiu Tipatge Ecosistema Quan triar-lo
Express JavaScript Molt baixa Gens Manual (Zod) No Mitjà Opcional (JSDoc/TS) Enorme Aprendre; APIs petites; màxim control
Fastify JavaScript/TS Baixa Poc JSON Schema integrada Sí (plugin) Alt Bo amb TS Gran Node amb exigència de rendiment i contracte
NestJS TypeScript Alta Molt class-validator Sí (@nestjs/swagger) Mitjà Excel·lent Gran Equips grans, domini complex, llarg termini
Hono TypeScript Baixa Poc Zod (adaptador) Sí (plugin) Molt alt Excel·lent Mitjà Edge, serverless, multi-runtime
FastAPI Python Baixa Mitjà Pydantic, de sèrie Sí, natiu i complet Mitjà-alt Excel·lent Gran APIs amb dades, ML, prototipatge ràpid i seriós
Django REST Python Mitjana Molt Serializers Sí (drf-spectacular) Mitjà-baix Feble Enorme CRUD sobre model relacional amb admin
Spring Boot Java Alta Molt Bean Validation Sí (springdoc) Alt Excel·lent Enorme Empresa, integracions, suport a llarg termini
ASP.NET Core C# Mitjana Mitjà Manual/FluentValidation Sí (Swashbuckle) Molt alt Excel·lent Gran Ecosistema Microsoft; cost per servidor
Laravel PHP Baixa Molt Form Requests Sí (paquets) Mitjà Mitjà Enorme Web PHP, allotjament barat, lliurament ràpid
Rails API Ruby Baixa Molt Model Sí (paquets) Mitjà-baix Feble Gran Prototips, startups, productivitat inicial
Go + Gin Go Mitjana Poc Manual/tags Parcial Molt alt Excel·lent Mitjà Serveis d'alt rendiment, contenidors mínims

Com llegir la columna de rendiment. És una ordenació aproximada de peticions per segon en proves sintètiques de «retornar un JSON petit». Gairebé mai no descriu la teva API real, perquè tan bon punt hi ha una consulta a base de dades, una crida de xarxa o una plantilla, el framework deixa de ser el coll d'ampolla. Un GET /v1/cafes que triga 40 ms dels quals 35 són la consulta SQL rendeix pràcticament igual a Express que a Fastify. Si el teu p99 és alt, mesura abans de culpar el framework: la resposta sol ser a l'apartat d'índexs i N+1 de 04-06.

  1. Criteris d'elecció honestos

Els criteris reals, ordenats pel pes que haurien de tenir:

1. El llenguatge que domina el teu equip. És el criteri número u, de molt. Un equip expert en Python lliurarà abans i amb menys errors amb FastAPI que amb el framework Node més ràpid del món. El cost d'aprendre un llenguatge nou —no la sintaxi, sinó els seus modismes, la seva depuració, el seu empaquetatge, els seus paranys— es mesura en mesos de productivitat reduïda i en errors subtils en producció.

2. Contractació i mercat laboral. Si demà necessites dues persones més, les trobes? I a la teva ciutat, o a la teva franja horària? Triar un framework de nínxol perquè és elegant i descobrir que no hi ha ningú a qui contractar és un error car i freqüent.

3. Maduresa i horitzó de suport. Qui el manté? Hi ha una empresa al darrere, una fundació, una persona? Quina és la política de versions? Spring i Django porten més de quinze anys i continuaran; un framework de dos anys amb un sol mantenidor és una aposta. Aquest criteri pesa el doble si l'API és de llarg recorregut i el triple si ets en un sector regulat.

4. Encaix amb el problema. Un CRUD sobre model relacional amb panell d'administració demana DRF a crits. Una API a la vora amb latència mínima demana Hono. Un domini complex amb vint mòduls i quinze persones demana NestJS o Spring. Forçar l'eina contra el problema es paga cada setmana.

5. Ecosistema per al que necessites. No l'ecosistema en abstracte: existeix un client madur per a la teva base de dades, el teu proveïdor d'OAuth, la teva passarel·la de pagament, el teu sistema de cues? Descobrir la setmana sis que l'SDK de la teva passarel·la no existeix en aquell llenguatge és un problema seriós.

6. Rendiment. L'últim, tret que estiguis en un cas concret on importi: desenes de milers de peticions per segon, latència sub-10 ms, o una factura de servidors prou gran perquè un 30 % d'eficiència siguin diners reals. Per al 95 % de les APIs, la diferència entre frameworks és irrellevant davant d'una consulta sense índex.

I un advertiment sobre els benchmarks públics: mesuren un endpoint que retorna {"hello":"world"}, amb configuracions optimitzades per especialistes i sense base de dades, sense autenticació, sense validació i sense logs. Són útils per descartar ordres de magnitud i enganyosos per a tota la resta. Mesura la teva API real amb autocannon (04-06) abans de prendre cap decisió basada en velocitat.

  1. El que no depèn del framework

Aquesta és l'observació central de la lliçó. Repassa el curs:

Mòdul Depèn del framework?
1 — HTTP, REST, restriccions, HATEOAS No. És HTTP i arquitectura.
2 — Recursos, mètodes, codis, errors, paginació, versionat No. És disseny de contracte.
3 — Entorn, rutes, validació, persistència, autenticació, errors, proves Sí. Aquí canvia tot.
4 — Seguretat, OAuth, rate limiting, CORS, memòria cau, observabilitat Gairebé res. Els conceptes i les capçaleres són idèntics; només canvia la biblioteca.
5 — Postman, OpenAPI, contractes, CI/CD, gateways No. Tot funciona sobre HTTP.
6 — Casos d'estudi i evolució No.

De sis mòduls, un canvia. I ni tan sols sencer: la separació per capes de 03-03 —rutes, controladors, serveis, repositoris— es conserva a tots ells amb altres noms, perquè no és una idea d'Express.

Exemples concrets del que es transfereix sense canvis:

  • Que POST /v1/comandes exigeixi Idempotency-Key i retorni 201 amb Location és contracte. Igual als nou.
  • Que un ETag que coincideixi amb If-None-Match produeixi 304 ho dicta HTTP. Canvia la funció que l'escriu, no la regla.
  • Que el token JWT porti sub, rol i exp, i que la validació comprovi signatura, expiració i emissor, és OAuth i JWT. Canvia la biblioteca.
  • Que un error es retorni com {"error": {"codi", "missatge", "detalls"}} és el teu catàleg.
  • Que les etiquetes de les mètriques no incloguin com_5001 perquè rebenten la cardinalitat és una regla de Prometheus.

Conclusió pràctica: si saps dissenyar i operar APIs, aprendre un framework nou és qüestió d'una o dues setmanes. Si només saps Express, no saps fer APIs: saps fer servir Express. El coneixement transferible és el dels mòduls 1, 2, 4 i 5.

  1. Migrar entre frameworks: d'Express a Fastify

Suposem que la Botiga Aroma decideix migrar a Fastify per rendiment i per la generació automàtica d'OpenAPI. Què passa amb el projecte?

graph TD
    A[Projecte Express] --> B{Què es conserva?}
    B --> C[openapi.yaml<br/>El contracte no canvia]
    B --> D[serveis/<br/>Lògica de negoci pura]
    B --> E[repositoris/<br/>SQL i persistència]
    B --> F[esquemes Zod<br/>convertibles a JSON Schema]
    B --> G[proves d'integració<br/>Supertest sobre HTTP]
    B --> H[colleccio Postman<br/>i Newman]
    A --> I{Què es reescriu?}
    I --> J[rutes/<br/>Router → plugins]
    I --> K[middleware/<br/>hooks i decoradors]
    I --> L[app.js<br/>ordre de la cadena]
    I --> M[controladors/<br/>signatura req,res → async]

Es conserva tot el que no toca req i res: els serveis de src/serveis/, els repositoris de src/repositoris/, els mapejadors, src/errors/error-api.js, les migracions i —el més valuós— l'openapi.yaml i les proves d'integració de 03-08, perquè parlen HTTP i els és igual qui respongui. Aquelles proves són la xarxa de seguretat que fa possible la migració: si passen amb Fastify, la migració és correcta per definició.

Es reescriu la capa de lliurament: rutes, middlewares i la composició de l'aplicació.

Aquí hi ha el mateix endpoint, abans i després:

// ABANS — Express
rutesCafes.get('/', autenticar, validar(esquemaConsultaCafes, 'query'),
  asincron(async (req, res) => {
    const { dades, total } = await serveiCafes.llistar(req.validat.query);
    res.json({ dades: dades.map(cafeARepresentacio), total });
  }));

// DESPRÉS — Fastify
// - `autenticar` passa de middleware a `preHandler`.
// - `validar` desapareix: ho fa l'esquema del mateix framework.
// - `asincron` desapareix: Fastify captura les promeses rebutjades de sèrie.
// - `res.json(...)` se substitueix per retornar l'objecte.
fastify.get('/cafes', {
  schema: esquemaLlistarCafes,
  preHandler: [fastify.autenticar],
}, async (peticio) => {
  const { dades, total } = await serveiCafes.llistar(peticio.query);
  return { dades: dades.map(cafeARepresentacio), total };
});

El controlador és idèntic tret de la signatura. Els esquemes Zod es converteixen amb zod-to-json-schema, l'eina que ja vam veure a 05-02.

La recepta de migració, si mai t'hi toca:

  1. Congela el contracte. Ni un canvi funcional durant la migració. Si barreges totes dues coses, no sabràs si una fallada ve de la migració o de la funció nova.
  2. Assegura primer les proves d'integració. Són el criteri objectiu d'èxit. Si la cobertura dels endpoints és baixa, apuja-la abans de tocar res.
  3. Migra per recursos, no de cop. Amb un proxy al davant pots servir /v1/cafes des del servei nou i la resta des del vell. És el patró de la figuera estranguladora, i és el que fa viable migrar un sistema en producció.
  4. Compara respostes byte a byte. Executa la col·lecció de Postman de 05-01 contra tots dos i fes-ne el diff. Les sorpreses solen ser a les capçaleres i a l'ordre dels camps.
  5. Vigila els detalls petits, que són els que mosseguen: el format exacte de l'error de validació, l'ordre de les claus del JSON, si l'ETag és feble o fort, la codificació dels paràmetres repetits.

Compensa? Gairebé mai per rendiment tot sol. Compensa quan el framework actual bloqueja alguna cosa important: falta de tipatge en un equip que creix, absència d'un ecosistema que necessites, o un manteniment abandonat. «És més modern» no és una raó; és una despesa sense retorn.

  1. Runtimes alternatius i serverless

Dos eixos més, breument, perquè afecten l'elecció tant com el framework.

Runtimes de JavaScript:

Runtime Proposta Estat
Node.js L'estàndard; ecosistema npm complet El que fa servir la Botiga Aroma. Node 20 LTS.
Deno Segur per defecte (permisos explícits), TypeScript natiu, biblioteca estàndard pròpia Madur; compatibilitat amb npm ja bona
Bun Velocitat extrema, gestor de paquets i executor de proves integrats Jove però utilitzable; compatible amb la majoria d'Express

El detall més interessant de Deno per al que hem vist a 04-02: els permisos són explícits (--allow-net=api.botigaaroma.example), de manera que una dependència compromesa no pot llegir el teu disc ni obrir connexions a un servidor desconegut. És una defensa real contra els atacs de cadena de subministrament que vam esmentar amb npm audit.

Serverless. La teva API no s'executa com a procés permanent, sinó com a funcions que s'invoquen sota demanda: AWS Lambda amb API Gateway, Google Cloud Functions, Azure Functions, Cloudflare Workers.

Servidor permanent Serverless
Cost sense trànsit El del servidor Zero
Cost amb molt trànsit Predictible Es pot disparar
Escalat El configures tu Automàtic
Arrencada en fred No existeix De desenes de ms a segons
Connexions a base de dades Pool estable Problema seriós: cal fer servir un pooler
Estat en memòria Possible (memòria cau local) No fiable
Depuració local Senzilla Més incòmoda

Implicacions directes per al que hem construït: el rate limiting amb memòria local de 04-04 no funciona en serverless —cada invocació pot anar a una altra instància—, així que Redis passa de recomanable a obligatori; la memòria cau en procés desapareix; i el pool de connexions de 03-05 es converteix en un problema que exigeix un pooler extern.

Express s'executa a Lambda amb adaptadors (serverless-http), però Hono està dissenyat per a aquell entorn i arrenca en una fracció del temps. És un bon exemple de com l'entorn de desplegament —tema de 05-05— condiciona l'elecció del framework tant com el llenguatge.

Errors Comuns i Consells

  • Triar per benchmark. Els gràfics mesuren {"hello":"world"} sense base de dades ni autenticació. El teu p99 el domina la consulta SQL, no l'encaminador.
  • Triar per moda. El framework del qual tothom parla aquest any pot estar sense manteniment d'aquí a tres. Comprova qui el sosté i amb quina política de versions.
  • Triar un llenguatge que l'equip no domina. És la decisió que més projectes enfonsa. La sintaxi s'aprèn en una setmana; la depuració, l'empaquetatge i els paranys del runtime, en mesos.
  • Confondre el framework amb l'arquitectura. DRF o NestJS no et donen un bon disseny d'API: et donen una estructura. Un ViewSet mal fet servir exposa el teu model de dades i viola tot el mòdul 2.
  • Migrar sense proves. Sense les proves d'integració de 03-08, una migració és una reescriptura a cegues. Assegura-les abans de començar.
  • Barrejar migració i funcionalitat nova. Quan alguna cosa falli no sabràs de quina meitat ve. Congela el contracte.
  • Oblidar que Fastify elimina els camps no declarats a l'esquema de resposta. És una virtut de seguretat i la causa més freqüent de «el meu camp nou no surt».
  • Creure que «codi primer» t'estalvia pensar el contracte. Els tipus es generen; el disseny de 02-02 no.
  • Consell: aprèn un segon framework d'un altre llenguatge, no un altre de Node. Comparar Express amb Fastify ensenya poc; comparar Express amb FastAPI o Spring ensenya què és essencial i què és accidental.
  • Consell: mantén la lògica de negoci fora del framework. Si els teus serveis no importen res d'Express, migrar és canviar la capa de lliurament. És la raó de ser de la separació de 03-03, i el seu valor només s'aprecia el dia que cal.
  • Consell: escriu un ADR amb la decisió (04-01), amb els criteris i les alternatives descartades. D'aquí a dos anys algú preguntarà per què, i sense ADR la resposta serà «perquè sí».

Exercicis

Exercici 1: triar framework per a tres escenaris

Per a cada escenari, tria un framework, justifica'l amb almenys tres criteris de l'apartat 14 i esmenta l'alternativa que descartes i per què:

A) Una startup de tres persones amb experiència en Python llança l'MVP d'una API de reserves de restaurants en vuit setmanes. Necessiten un panell d'administració intern des del primer dia i preveuen canvis de model constants.

B) Un banc modernitza la seva API de consulta de moviments. Deu persones, requisits d'auditoria i traçabilitat, integració amb un proveïdor d'identitat corporatiu, i suport garantit a deu anys.

C) Una API de geolocalització que rep 50.000 peticions per segon, retorna respostes molt petites des d'una memòria cau en memòria, i ha de respondre en menys de 20 ms a tot el món.

Exercici 2: portar l'endpoint de ressenyes

Aquest és l'endpoint GET /v1/cafes/{id}/ressenyes a Express:

rutesCafes.get('/:id/ressenyes',
  autenticar,
  validar(esquemaIdCafe, 'params'),
  validar(esquemaConsultaRessenyes, 'query'),
  cacheDe({ maxEdat: 300, publica: true }),
  asincron(async (req, res) => {
    const { id } = req.validat.params;
    const { limit, desplacament, puntuacioMin } = req.validat.query;
    const { dades, total } = await serveiRessenyes.llistarDeCafe(id, {
      limit, desplacament, puntuacioMin,
    });
    res.set('Link', construirCapcaleraLink(req, total, limit, desplacament));
    res.json({ dades: dades.map(ressenyaARepresentacio), total });
  }));

Porta'l a Fastify mantenint el mateix contracte: mateixos paràmetres i validacions, mateixes capçaleres Cache-Control i Link, i el mateix format d'error parametre_invalid. Indica quines peces desapareixen, quines canvien de nom i què s'ha d'afegir explícitament que a Express era en un middleware.

Exercici 3: què es conserva d'aquest curs

Un company s'incorpora a un projecte en Spring Boot venint d'aquest curs, fet en Express. Redacta una guia d'una pàgina que li digui: quins coneixements dels mòduls 1, 2, 4 i 5 aplica tal qual (amb tres exemples concrets), què ha de reaprendre del mòdul 3 i quin és l'equivalent a Spring de cinc peces del nostre projecte (middleware/autenticacio.js, middleware/errors.js, repositoris/cafes-sqlite.js, esquemes/cafes.js i observabilitat/metriques.js).

Solucions

Solució 1

A) Startup de reserves → Django REST Framework.

Criteri Anàlisi
Llenguatge de l'equip Ja dominen Python. És el criteri de més pes i descarta d'entrada Node i Java.
Encaix amb el problema CRUD sobre model relacional (restaurants, taules, reserves, clients) amb panell d'administració: és literalment el cas per al qual DRF existeix.
Termini Vuit setmanes. El django-admin gratuït estalvia setmanes davant de construir un panell. Els ViewSets donen els set endpoints per recurs gairebé sense codi.
Ecosistema Migracions, autenticació i admin resolts; l'equip només escriu regles de negoci.

Alternativa descartada: FastAPI. Millor contracte i millor documentació automàtica, però no porta ORM, ni migracions, ni admin: caldria muntar SQLAlchemy, Alembic i un panell, i allà se'n van les vuit setmanes. Es reconsideraria als dos anys si l'API deixa de ser un CRUD i el contracte comença a divergir del model.

B) Banc → Spring Boot.

Criteri Anàlisi
Maduresa i horitzó Més de quinze anys, versions amb suport estès i respatller comercial de VMware/Broadcom. En un sector regulat, «qui respon» és un requisit, no una preferència.
Ecosistema Spring Security amb OIDC integrat amb el proveïdor corporatiu (04-03); Actuator dona salut i mètriques Prometheus (04-07) de fàbrica; auditoria i traçabilitat són peces estàndard.
Mida de l'equip Deu persones: l'estructura opinada evita deu maneres diferents d'organitzar el codi i accelera les incorporacions.
Contractació El mercat de Java empresarial és el més profund, especialment a la banca.
Precisió decimal BigDecimal de sèrie: en diners, un requisit dur.

Alternativa descartada: NestJS. Dona una arquitectura semblant amb TypeScript i seria una elecció defensable, però perd en horitzó de suport, en maduresa de l'ecosistema de seguretat i auditoria empresarial, i en profunditat del mercat laboral bancari. A més, JavaScript no té tipus decimal natiu, cosa que en un sistema financer és fricció constant.

C) Geolocalització a 50.000 rps → Go, o Hono sobre Cloudflare Workers.

Criteri Anàlisi
Rendiment Aquí sí que és un criteri de primer ordre: 50.000 rps amb respostes de memòria cau és justament l'escenari on el framework és el coll d'ampolla.
Encaix Respostes petites des de memòria, sense base de dades per petició: la feina és encaminar, cercar i serialitzar.
Latència global (<20 ms) Impossible des d'una sola regió: exigeix presència a la vora. Cloudflare Workers amb Hono ho resol per disseny; amb Go caldrien desplegaments multiregió i encaminament per anycast.
Cost Go dona imatges de pocs megues i consum mínim de memòria: menys màquines per al mateix trànsit.

Decisió: Hono a la vora si el conjunt de dades cap en un magatzem distribuït tipus KV; Go multiregió si les dades són grans o el càlcul és intensiu. Descartats Express, NestJS i DRF: a aquell volum la diferència d'eficiència es tradueix directament en factura, i DRF a més arrossega un ORM que aquest cas no fa servir.

Solució 2

// rutes/ressenyes.js — Fastify

// L'esquema declara TOT el contracte d'entrada i de sortida.
const esquemaRessenyesDeCafe = {
  tags: ['Ressenyes'],
  summary: 'Llista les ressenyes d\'un cafè',
  operationId: 'obtenirRessenyesDeCafe',
  security: [{ bearerJWT: [] }],
  params: {
    type: 'object',
    required: ['id'],
    properties: {
      id: { type: 'string', pattern: '^caf_[A-Za-z0-9]+$' },
    },
  },
  querystring: {
    type: 'object',
    additionalProperties: false,
    properties: {
      limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
      desplacament: { type: 'integer', minimum: 0, maximum: 10000, default: 0 },
      puntuacioMin: { type: 'integer', minimum: 1, maximum: 5 },
    },
  },
  response: {
    200: {
      type: 'object',
      properties: {
        dades: { type: 'array', items: { $ref: 'Ressenya#' } },
        total: { type: 'integer' },
      },
    },
    400: { $ref: 'Error#' },
    404: { $ref: 'Error#' },
  },
};

export default async function rutesRessenyes(fastify) {
  fastify.get('/cafes/:id/ressenyes', {
    schema: esquemaRessenyesDeCafe,
    preHandler: [fastify.autenticar],
    // La memòria cau HTTP s'aplica amb un hook onSend, perquè a Fastify
    // les capçaleres de resposta es toquen al cicle de vida, no en un middleware.
    onSend: async (peticio, resposta, cos) => {
      resposta.header('Cache-Control', 'public, max-age=300');
      resposta.header('Vary', 'Accept-Encoding, Origin');
      return cos;
    },
  }, async (peticio, resposta) => {
    const { id } = peticio.params;
    const { limit, desplacament, puntuacioMin } = peticio.query;

    const { dades, total } = await fastify.serveis.ressenyes.llistarDeCafe(id, {
      limit, desplacament, puntuacioMin,
    });

    // La capçalera Link es continua construint a mà: és lògica del contracte,
    // no una cosa que cap framework resolgui per tu.
    resposta.header('Link', construirCapcaleraLink(peticio, total, limit, desplacament));

    return { dades: dades.map(ressenyaARepresentacio), total };
  });
}

I el manejador d'errors global, imprescindible perquè el 400 automàtic de Fastify parli el nostre idioma:

// app.js — tradueix els errors de validació de Fastify al nostre catàleg
fastify.setErrorHandler((error, peticio, resposta) => {
  if (error.validation) {
    const enQuery = error.validationContext === 'querystring';
    return resposta.status(400).send({
      error: {
        codi: enQuery ? 'parametre_invalid' : 'dades_invalides',
        missatge: enQuery
          ? 'Paràmetres de consulta invàlids.'
          : 'El cos conté camps invàlids.',
        detalls: error.validation.map((v) => ({
          camp: v.instancePath.replace('/', '') || v.params?.additionalProperty,
          problema: v.message,
        })),
      },
    });
  }
  // ErrorApi i la resta es tradueixen igual que a src/middleware/errors.js
  return gestorErrors(error, peticio, resposta);
});

Balanç de la migració d'aquest endpoint:

Peça a Express A Fastify
validar(esquemaIdCafe, 'params') Desapareix: schema.params
validar(esquemaConsultaRessenyes, 'query') Desapareix: schema.querystring
asincron(...) Desapareix: captura les promeses de sèrie
autenticar Canvia de nom: preHandler
cacheDe({...}) Canvia de forma: hook onSend
res.set('Link', ...) resposta.header('Link', ...)
res.json({...}) return {...}
Format de l'error 400 S'ha d'afegir: setErrorHandler
construirCapcaleraLink(...) Idèntic: és lògica del contracte
ressenyaARepresentacio(...) Idèntic: és un mapejador pur

El que s'ha d'afegir explícitament és el més important de la llista: a Express controlàvem el format del 400 perquè l'escrivíem nosaltres; a Fastify, si no es registra setErrorHandler, la validació automàtica retorna el format del framework i trenca el contracte d'errors de 02-04 sense que ningú ho noti fins que un consumidor es queixa. És l'exemple perfecte que les garanties automàtiques d'un framework s'han de reconduir al teu contracte, no a l'inrevés.

Solució 3

Guia d'incorporació: d'Express a Spring Boot

El que apliques tal qual (mòduls 1, 2, 4 i 5).

El 80 % del que saps continua sent vàlid, perquè descriu HTTP i disseny, no Express. Tres exemples concrets:

  1. El disseny del contracte (mòdul 2). Que els recursos siguin substantius en plural, que les transicions d'estat s'expressin com a subrecursos (POST /comandes/{id}/pagament) i no com un PATCH sobre estat, que el 201 porti Location, que un filtre sense resultats sigui 200 amb llista buida i no 404, i que la paginació sigui obligatòria amb limit i desplacament. Res d'això no canvia: ho escriuràs amb @PostMapping en lloc de router.post, i prou.
  2. Memòria cau i concurrència (04-06). ETag, If-None-Match304, If-Match412, Cache-Control amb max-age. Spring ho exposa amb ResponseEntity.ok().eTag(...) i ShallowEtagHeaderFilter, però les regles són les d'HTTP i les decisions de política són les mateixes que vas prendre.
  3. Tot el mòdul 5. La col·lecció de Postman funciona sense tocar una línia, perquè parla HTTP. L'openapi.yaml és el mateix document. La canalització de CI canvia npm ci per mvn verify i poc més. I un gateway al davant no distingeix què hi ha al darrere.

El que has de reaprendre (mòdul 3).

Només la capa de lliurament i les seves eines: anotacions en lloc de middlewares, Bean Validation en lloc de Zod, JPA o JDBC en lloc de better-sqlite3, JUnit i MockMvc en lloc de node:test i Supertest, i —el més aliè venint de Node— el contenidor d'injecció de dependències: a Spring no instancies els teus serveis, els declares i el framework els construeix i injecta.

Taula d'equivalències:

Peça de la Botiga Aroma Equivalent a Spring Boot Nota
src/middleware/autenticacio.js SecurityFilterChain de Spring Security amb oauth2ResourceServer().jwt() Spring valida signatura, expiració, emissor i àmbits; els rols es comproven amb @PreAuthorize("hasRole('ADMINISTRADOR')") sobre el mètode, més fi que el nostre exigirRol.
src/middleware/errors.js @RestControllerAdvice amb mètodes @ExceptionHandler Mateix concepte exacte: un punt únic que tradueix excepcions a HTTP. ErrorApi passa a ser una excepció pròpia; convé fer servir ProblemDetail (RFC 9457) o mantenir el teu format amb un DTO.
src/repositoris/cafes-sqlite.js Interfície CafeRepository extends JpaRepository<Cafe, String> El patró repositori no és idea nostra: Spring Data l'implementa sol a partir de la interfície. Els mètodes de consulta es dedueixen del nom (findByTorrefaccioAndPreuCentimsLessThan). Si prefereixes SQL explícit, @Query o JdbcTemplate.
src/esquemes/cafes.js (Zod) Anotacions de Bean Validation (@NotNull, @Size, @Min, @Pattern) sobre el DTO, activades amb @Valid Menys expressiu que Zod per a transformacions i unificat amb el contracte; els missatges es personalitzen a messages.properties.
src/observabilitat/metriques.js (prom-client) Micrometer + spring-boot-starter-actuator Mètriques HTTP, JVM i del pool de connexions pràcticament gratis a /actuator/prometheus; /actuator/health dona liveness i readiness separats, exactament el que vam escriure a mà a 04-07. Les regles de cardinalitat són idèntiques: no etiquetis mai amb com_5001.

Consell final per a la incorporació: dedica el primer dia a llegir l'openapi.yaml del projecte, no el codi. El contracte és el que ja saps llegir, i et donarà el mapa complet del sistema abans d'enfrontar-te a una sola anotació de Spring.

Conclusió

Has vist el mateix endpoint —GET /v1/cafes, amb els seus filtres, la seva paginació obligatòria, el seu 400 del catàleg i la seva conversió de cèntims a euros— resolt en Express, Fastify, NestJS, Hono, FastAPI, Django REST Framework, Spring Boot, ASP.NET Core i Go. I amb això has vist els tres models que es reparteixen el panorama: el minimalista, on tu escrius totes les garanties i per això les entens; el d'esquema com a centre, on un mateix document serveix de validació, serialització i documentació —Fastify i FastAPI en són els millors exponents, i resolen d'arrel la deriva de contracte que a 05-02 vam haver de combatre amb eines—; i l'opinat, NestJS, Spring i DRF, que a canvi de més cerimònia donen estructura idèntica, injecció de dependències i una productivitat enorme quan el problema encaixa amb el que el framework espera.

Els criteris d'elecció, en el seu ordre real: el llenguatge que domina el teu equip, la contractació, la maduresa i l'horitzó de suport, l'encaix amb el problema, l'ecosistema concret que necessites i, en darrer lloc tret de casos molt específics, el rendiment —perquè els benchmarks mesuren {"hello":"world"} sense base de dades i el teu p99 el domina la consulta SQL de 04-06—. I saps que una migració es conserva o es perd segons una única cosa: si la teva lògica de negoci importa el framework o no. A la Botiga Aroma no ho fa, i per això src/serveis/, src/repositoris/, els mapejadors, openapi.yaml, la col·lecció de Postman i —sobretot— les proves d'integració de 03-08 sobreviurien intactes a un canvi a Fastify; només es reescriurien src/rutes/, src/middleware/ i src/app.js.

El més important d'aquesta lliçó és el que no canvia. Dels sis mòduls del curs, només el tercer depèn del framework, i ni tan sols sencer. Que una comanda exigeixi Idempotency-Key, que un ETag coincident produeixi 304, que els errors portin un codi estable del catàleg, que la SPA necessiti CORS i el panell un token amb àmbits, que les etiquetes de les mètriques no incloguin identificadors: res d'això no és Express. Si saps dissenyar i operar APIs, aprendre un framework nou són dues setmanes; si només saps Express, saps fer servir Express.

Tornem ara al projecte i a un cap que 05-02 va deixar solt. Tenim un contracte complet i validat com a document, però encara res no garanteix que el servidor el compleixi, ni que un canvi al YAML no trenqui la SPA sense avisar. A 05-04, Contractes, mocks i proves automatitzades d'API, tanquem aquell cercle: aixecarem un mock amb Prism des d'openapi.yaml perquè la SPA avanci en paral·lel, farem servir msw al front i nock per a les crides sortints a RàpidEnviaments, validarem les respostes reals contra els esquemes dins de les proves Supertest que ja tenim, detectarem canvis trencadors amb oasdiff aplicant les regles de 02-07, veurem quan el contract testing amb Pact compensa i quan és sobreenginyeria, i organitzarem el recorregut de compra complet com a prova d'extrem a extrem, amb la taula de què s'executa en desar, al pull request, al desplegament i en producció.

Curs de REST API: Principis de Disseny i Desenvolupament d'APIs RESTful

Mòdul 1: Introducció a les APIs RESTful

Mòdul 2: Disseny d'APIs RESTful

Mòdul 3: Desenvolupament d'APIs RESTful

Mòdul 4: Bones Pràctiques i Seguretat

Mòdul 5: Eines i Frameworks

Mòdul 6: Casos d'Estudi i Projectes

© Copyright 2026. Tots els drets reservats