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
- Per què existeix aquesta lliçó
- Què s'espera avui d'un framework d'API
- L'endpoint de referència
- Express: el mínim, tot a càrrec teu
- Fastify: esquemes, plugins i velocitat
- NestJS: arquitectura opinada per a equips grans
- Hono: lleuger i multi-runtime
- FastAPI (Python): el tipatge com a contracte
- Django REST Framework (Python): serializers i viewsets
- Spring Boot (Java): l'estàndard empresarial
- ASP.NET Core (C#): minimal APIs i rendiment
- Mencions: Laravel, Rails API i Go
- La taula comparativa
- Criteris d'elecció honestos
- El que no depèn del framework
- Migrar entre frameworks: d'Express a Fastify
- Runtimes alternatius i serverless
- 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.
- 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.
- 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:
limitper defecte 20, màxim 100;desplacamentmàxim 10.000.torrefaccionomés admetclar,mitjaofosc.- Un paràmetre invàlid produeix
400amb{"error": {"codi": "parametre_invalid", ...}}. - El preu s'emmagatzema en cèntims enters i se serialitza en euros amb dos decimals.
- 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ó.
- 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.
- 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'esquemaquerystringel fa el framework, i produeix el400sol. Guanyes garantia —no te'n pots oblidar— i perds control sobre el format de l'error, que s'ha de personalitzar ambsetErrorHandlerperquè encaixi amb el nostre catàleg. - L'esquema de resposta compila un serialitzador. Fastify converteix aquell
response.200en una funció de serialització especialitzada, més ràpida queJSON.stringifygenè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.usea Express, que és global. Això permet, per exemple, aplicar un rate limiting diferent a/v1/sessionssense 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.
- 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.
- 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.
- 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
422automàtic (personalitzable al nostre400). - Conversió de tipus:
limit=20arriba com a text i es lliura com aint. - Documentació OpenAPI 3.1 completa, servida a
/docsamb Swagger UI i a/redocamb Redoc, sense una línia extra. - Serialització amb els àlies
preu_euros→preuEuros, que resol l'etern xoc entre elsnake_casede Python i elcamelCasedel JSON. - Injecció de dependències amb
Depends, que a més fa trivial substituirusuari_actuala 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.
- 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.urlsQuaranta 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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/comandesexigeixiIdempotency-Keyi retorni201ambLocationés contracte. Igual als nou. - Que un
ETagque coincideixi ambIf-None-Matchprodueixi304ho dicta HTTP. Canvia la funció que l'escriu, no la regla. - Que el token JWT porti
sub,roliexp, 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_5001perquè 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.
- 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:
- 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.
- 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.
- Migra per recursos, no de cop. Amb un proxy al davant pots servir
/v1/cafesdes 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ó. - 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.
- 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.
- 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:
- 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 unPATCHsobreestat, que el201portiLocation, que un filtre sense resultats sigui200amb llista buida i no404, i que la paginació sigui obligatòria amblimitidesplacament. Res d'això no canvia: ho escriuràs amb@PostMappingen lloc derouter.post, i prou. - Memòria cau i concurrència (04-06).
ETag,If-None-Match→304,If-Match→412,Cache-Controlambmax-age. Spring ho exposa ambResponseEntity.ok().eTag(...)iShallowEtagHeaderFilter, però les regles són les d'HTTP i les decisions de política són les mateixes que vas prendre. - 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 canvianpm cipermvn verifyi 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
- Què és una API?
- Història i evolució de les APIs
- Fonaments d'HTTP per a APIs
- Principis bàsics de REST
- Model de maduresa de Richardson i HATEOAS
- REST vs. SOAP
- REST davant de GraphQL, gRPC i webhooks
Mòdul 2: Disseny d'APIs RESTful
- Principis de disseny d'APIs RESTful
- Recursos i URIs
- Mètodes HTTP
- Codis d'estat HTTP
- Representacions, capçaleres i negociació de contingut
- Filtratge, ordenació, paginació i cerca
- Versionat d'APIs
- Documentació d'APIs
Mòdul 3: Desenvolupament d'APIs RESTful
- Configuració de l'entorn de desenvolupament
- Creació d'un servidor bàsic
- Gestió de peticions i respostes
- Validació de dades d'entrada
- Persistència i capa d'accés a dades
- Autenticació i autorització
- Gestió d'errors
- Proves i validació
Mòdul 4: Bones Pràctiques i Seguretat
- Bones pràctiques en el disseny d'APIs
- Seguretat en APIs RESTful
- OAuth 2.0 i OpenID Connect a la pràctica
- Rate limiting i throttling
- CORS i polítiques de seguretat
- Memòria cau HTTP i rendiment
- Observabilitat: logs, mètriques i traces
Mòdul 5: Eines i Frameworks
- Postman per a proves d'APIs
- Swagger i OpenAPI per a documentació
- Frameworks populars per a APIs RESTful
- Contractes, mocks i proves automatitzades d'API
- Integració contínua i desplegament
- API gateways i portals de desenvolupador
