POST /api/comandes continua fent registrarComanda(peticio.body) amb el que arribi. Helmet, CORS i el limitador de peticions no miren el contingut: un cos amb quantitat: -5, un sessioId que és un array o un correu de 40.000 caràcters hi passa sense despentinar-se. Aquest és el forat que tanquem ara. La validació és una de les coses que Express no fa per tu (ho vam dir a 06-01, taula inclosa) i és també una de les que més conseqüències té quan falta. En aquesta lliçó decidim on viu, triem una eina, escrivim els esquemes d'Escena Viva i construïm un middleware genèric que es col·loca a la ruta i deixa les dades ja netes i convertides.
Contingut
- Per què no es confia mai en el client
- Què es valida i on ha de viure la validació
- Validació a mà i per què esdevé immantenible
- zod: el validador triat
- Els esquemes d'Escena Viva
- El middleware genèric
validar(esquema, origen) - Coerció i normalització
- La resposta d'error útil
- Usos menys obvis: respostes i configuració
- Sanejament enfront de validació
- Per què no es confia mai en el client
public/app.js valida el formulari abans d'enviar: comprova que la quantitat estigui entre 1 i 6, que el correu tingui arrova i que hi hagi una sessió seleccionada. I tot i així, el servidor ho ha de tornar a validar tot. Motius, ordenats de més evident a menys:
- El client no és el teu formulari. Qualsevol pot cridar l'API amb
curl, amb Postman o amb un script: el teu HTML és un suggeriment, no un control. - El client es pot modificar. Les eines de desenvolupament del navegador permeten canviar el
maxd'uninputen dos segons. - Hi pot haver més clients. Una app mòbil, un panell de taquilla, una integració amb un promotor. Cadascun amb les seves pròpies fallades.
- Els intermediaris alteren dades. Proxies, redireccions, reintents amb cos truncat.
- El client valida per a l'experiència; el servidor valida per a la integritat. El primer evita frustració, el segon evita corrupció.
La formulació clàssica: la validació al client és cortesia; la validació al servidor és obligació. I no es tracta només d'atacants: un desplegament del front-end que enviï quantitat com a cadena en comptes de número per un error de tipatge provoca exactament el mateix desastre, sense mala intenció.
- Què es valida i on ha de viure la validació
| Origen | Què arriba | Risc típic |
|---|---|---|
req.body |
Cos JSON de la comanda | Tipus incorrectes, camps absents, camps de més |
req.params |
:id, :idSessio |
Format arbitrari, cadenes enormes, caràcters de control |
req.query |
Filtres i paginació | perPagina=999999 que tomba la resposta |
req.headers |
X-Canal-Venda, Accept |
Injecció als registres, valors desmesurats |
I la pregunta més important: on es col·loca aquesta validació? El recorregut és Client → validar() → Controlador → Servei → Domini, i hi ha dos punts de rebuig ben diferents: el middleware validar() retorna 400 quan la forma és invàlida, i el domini retorna 409 quan l'aforament no dona. Són dos nivells amb responsabilitats diferents, i confondre'ls és un error de disseny freqüent:
| Validació d'entrada (la vora) | Invariants del domini | |
|---|---|---|
| Pregunta que respon | Tenen les dades la forma correcta? | És aquesta operació vàlida ara? |
| Exemple | quantitat és un enter entre 1 i 6 |
La sessió ses-001-1 té 3 entrades lliures |
| On viu | src/esquemes/ + middleware validar |
src/domini/sessio.js, gestor-vendes.js |
| Depèn de l'estat / coneix HTTP | No / sí, és la vora | Sí / no, mai |
| Estat HTTP resultant | 400 o 422 | 409 |
La regla: la vora garanteix la forma; el domini garanteix les regles. Sessio.vendre() no ha de comprovar si quantitat és un número —això ja està garantit quan hi arriba— però sí que ha de comprovar si hi ha aforament, perquè això depèn de l'estat i pot canviar entre la validació i la venda. I a l'inrevés: si esborres el middleware de validació, el domini no s'ha de trencar de forma catastròfica, però tampoc no és feina seva generar missatges amables per a un client HTTP.
- Validació a mà i per què esdevé immantenible
Recorda com va quedar rutes-comandes.js al mòdul 4:
// Extracte del modul 4. Funcionava. I creixeria sense control.
function validarComanda(dades) {
if (typeof dades !== 'object' || dades === null || Array.isArray(dades)) {
throw Object.assign(new Error('El cos ha de ser un objecte'), { codi: 'JSON_INVALID' });
}
if (typeof dades.sessioId !== 'string' || !/^ses-\d{3}-\d$/.test(dades.sessioId)) {
throw Object.assign(new Error('sessioId invalid'), { codi: 'PARAMETRE_INVALID' });
}
if (!Number.isInteger(dades.quantitat) || dades.quantitat < 1) {
throw Object.assign(new Error('quantitat invalida'), { codi: 'QUANTITAT_INVALIDA' });
}
if (dades.quantitat > LIMIT_ENTRADES_PER_COMANDA) {
throw Object.assign(new Error('maxim 6 entrades'), { codi: 'LIMIT_PER_COMANDA' });
}
// ...i ara el correu, i el canal, i els camps opcionals, i el descompte...
}Cinc problemes concrets, i tots empitjoren amb el temps:
- S'atura al primer error. El client arregla
sessioId, torna a enviar, i descobreix que també fallavaquantitat: tres viatges per a tres errors. - No retorna el resultat convertit. Valides
dades.quantitatperò continues fent servir l'objecte original sense normalitzar. - Els camps de més hi passen. Si el client envia
{ quantitat: 2, esAdministrador: true }, aquest camp arriba intacte al domini. - És impossible de reutilitzar. La mateixa comanda validada des d'una cua de missatges o un script d'importació necessita copiar i enganxar.
- No es pot documentar automàticament. No hi ha descripció de la forma esperada; només codi imperatiu que cal llegir sencer.
Cinquanta línies per a quatre camps: amb vint endpoints, això és la meitat del projecte.
- zod: el validador triat
zod inverteix l'enfocament: en comptes d'escriure comprovacions, descrius la forma de les dades, i la biblioteca en deriva la validació, la conversió i els missatges. S'instal·la amb npm install zod, i npm view zod license dependencies confirma el que buscaves al mòdul 5: MIT i zero dependències.
Comparativa breu i honesta:
| zod | express-validator | Joi | |
|---|---|---|---|
| Acoblament a Express | Cap | Alt: és middleware | Cap |
| Estil | Esquema declaratiu | Cadena de comprovacions per camp | Esquema declaratiu |
| Sortida i reutilització fora d'HTTP | Objecte convertit i podat; sí | Modifica req i exposa validationResult; no |
Objecte convertit; sí |
| Inferència de tipus | Excel·lent (amb TypeScript, de franc) | Escassa | Bona |
| Dependències i corba | Zero, corba suau | Diverses, corba suau | Diverses, corba mitjana |
Triem zod per tres raons que encaixen amb el curs: zero dependències (criteri del mòdul 5), independència d'Express (el mateix esquema serveix per a l'API, per a un script d'importació i per a les proves del mòdul 9) i perquè l'esquema és una font única de veritat que documenta l'entrada millor que qualsevol comentari. L'essencial de la seva API: es declara un objecte amb z.object({ nom: z.string().min(1).max(80), edat: z.number().int().positive(), correu: z.email(), canal: z.enum([...]), notes: z.string().optional(), actiu: z.boolean().default(true) }) i s'executa amb esquema.safeParse(dades) —que retorna { success, data } o { success, error }— o amb esquema.parse(dades), que retorna les dades o llança un ZodError.
Detall clau: z.object() descarta per defecte les claus no declarades. { quantitat: 2, esAdministrador: true } surt del validador com a { quantitat: 2 }, així que el problema 3 de l'apartat anterior queda resolt d'ofici. I si prefereixes rebutjar en comptes de descartar, .strict() fa que la clau sobrera sigui un error.
- Els esquemes d'Escena Viva
// src/esquemes/comanda.js
const { z } = require('zod');
const LIMIT_ENTRADES_PER_COMANDA = 6;
// Peces reutilitzables. Identificador de sessio: ses-NNN-M (ses-001-1...).
const idSessio = z
.string({ error: 'sessioId ha de ser una cadena' })
.trim()
.regex(/^ses-\d{3}-\d$/, 'sessioId ha de tenir el format ses-NNN-M');
const idEsdeveniment = z.string().trim().regex(/^evt-\d{3}$/, "L'esdeveniment ha de ser evt-NNN");
const quantitatEntrades = z.coerce
.number({ error: 'quantitat ha de ser un numero' })
.int('quantitat ha de ser un enter')
.min(1, 'Has de demanar com a minim 1 entrada')
.max(LIMIT_ENTRADES_PER_COMANDA, `Maxim ${LIMIT_ENTRADES_PER_COMANDA} entrades per comanda`);
const correuComprador = z
.email('El correu del comprador no es valid')
.max(254, 'El correu es massa llarg')
.transform((valor) => valor.trim().toLowerCase());
const canalVenda = z.enum(['web', 'taquilla', 'telefon'], { error: 'canal no valid' });
/** Cos de POST /api/comandes */
const esquemaCrearComanda = z.object({
sessioId: idSessio,
quantitat: quantitatEntrades,
correu: correuComprador,
canal: canalVenda.default('web'),
comentari: z.string().trim().max(280).optional(),
});
/** Parametres de /api/esdeveniments/:id i query de GET /api/esdeveniments */
const esquemaParametresEsdeveniment = z.object({ id: idEsdeveniment });
const esquemaConsultaEsdeveniments = z.object({
sala: z.enum(['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera']).optional(),
exhaurit: z.enum(['true', 'false']).transform((v) => v === 'true').optional(),
pagina: z.coerce.number().int().min(1).default(1),
perPagina: z.coerce.number().int().min(1).max(50).default(10),
});
module.exports = {
esquemaCrearComanda, esquemaParametresEsdeveniment, esquemaConsultaEsdeveniments,
};Compara-ho amb les cinquanta línies imperatives de l'apartat 3: aquí hi ha més regles, en menys espai, i es llegeixen com una especificació. I LIMIT_ENTRADES_PER_COMANDA continua sent la mateixa constant del mòdul 4: la regla no ha canviat, només el lloc on s'expressa.
- El middleware genèric
validar(esquema, origen)
validar(esquema, origen)Un sol middleware configurable que es col·loca a qualsevol ruta. És una factoria, com les de 06-04.
// src/middleware/validar.js
const { ErrorDeValidacio } = require('../errors.js'); // 06-07
const ORIGENS_VALIDS = ['body', 'params', 'query', 'headers'];
// Retorna un middleware que valida una part de la peticio contra un esquema.
// El resultat ja convertit i podat es deixa a req.dadesValidades[origen].
function validar(esquema, origen = 'body') {
// Error de programacio: es detecta en arrencar, no en produccio.
if (!ORIGENS_VALIDS.includes(origen)) {
throw new Error(`Origen de validacio no suportat: ${origen}`);
}
return function validarPeticio(peticio, resposta, seguent) {
const resultat = esquema.safeParse(peticio[origen]);
if (!resultat.success) {
// Convertim els problemes de zod al nostre format de detalls.
const detalls = resultat.error.issues.map((p) => ({
camp: p.path.join('.') || origen, motiu: p.message, tipus: p.code,
}));
seguent(new ErrorDeValidacio('Les dades enviades no son valides', detalls));
return;
}
// IMPORTANT: no reassignem peticio.query (nomes lectura a Express 5)
// ni peticio.params. Desem el resultat en una propietat propia.
peticio.dadesValidades = peticio.dadesValidades ?? {};
peticio.dadesValidades[origen] = resultat.data;
seguent();
};
}
module.exports = { validar, ORIGENS_VALIDS };Ús a les rutes, i controlador ja sense comprovacions:
rutesComandes.post('/', validar(esquemaCrearComanda, 'body'), crearComanda);
rutesEsdeveniments.get('/', validar(esquemaConsultaEsdeveniments, 'query'), llistarEsdeveniments);
rutesEsdeveniments.get('/:id', validar(esquemaParametresEsdeveniment, 'params'), obtenirEsdeveniment);
// src/controladors/comandes.js — les dades ja tenen la forma correcta:
// els enters son enters, el correu es en minuscules i no hi ha camps de mes.
async function crearComanda(peticio, resposta) {
const comanda = await registrarComanda(peticio.dadesValidades.body);
resposta.status(201).location(`/api/comandes/${comanda.id}`).json(comanda.toJSON());
}Per què no es reassigna req.query
A Express 4 el patró habitual era req.query = resultat.data, i funcionava. A Express 5 req.query és un getter sense setter, i aquesta línia llança TypeError: Cannot set property query of #<IncomingMessage> which has only a getter; si migres codi antic, és un dels primers errors que veuràs. Desar a req.dadesValidades no és només una manera d'esquivar la limitació: és millor disseny, perquè deixa explícit al controlador que les dades que fa servir han passat per un esquema i conserva intacte el que va arribar del client per si el registre ho ha de comparar.
- Coerció i normalització
Tot el que arriba per HTTP és text. req.params i req.query són sempre cadenes —a GET /api/esdeveniments?pagina=2, req.query.pagina és '2', no 2—; només req.body amb Content-Type: application/json conserva tipus, i només si el client els va enviar bé. Per això convertir és part de validar, no un pas posterior:
// Coercio: accepta '2' i 2, retorna sempre el numero 2.
pagina: z.coerce.number().int().min(1).default(1),
// Normalitzacio: retalla espais i baixa a minuscules.
correu: z.email().transform((valor) => valor.trim().toLowerCase()),
// Coercio de boolea des de la query, que mai no porta booleans.
exhaurit: z.enum(['true', 'false']).transform((valor) => valor === 'true').optional(),| Operació | Què fa | Exemple a Escena Viva |
|---|---|---|
| Coerció | Converteix el tipus | '2' → 2 a quantitat |
| Normalització | Unifica representacions equivalents | ' [email protected] ' → '[email protected]' |
| Valor per defecte i poda | Omple el que falta i descarta el no declarat | canal absent → 'web'; esAdministrador desapareix |
Sense baixar el correu a minúscules, [email protected] i [email protected] són dos compradors diferents: els duplicats de dades mestres gairebé sempre neixen d'una normalització que va faltar.
Compte amb la coerció excessiva.
z.coerce.number()accepta''com a0itruecom a1, perquè fa servirNumber()per sota. Si això no et va bé, sigues explícit:z.string().regex(/^\d+$/).transform(Number). Coerció amable a la query (on tot és text per força), coerció estricta al cos JSON (on el client va poder enviar el tipus correcte).
- La resposta d'error útil
Mantenim el format fixat al mòdul 4 i hi afegim detalls:
{
"error": {
"codi": "DADES_INVALIDES",
"missatge": "Les dades enviades no son valides",
"estat": 400,
"detalls": [
{ "camp": "quantitat", "motiu": "Maxim 6 entrades", "tipus": "too_big" },
{ "camp": "correu", "motiu": "El correu no es valid", "tipus": "invalid_format" }
],
"idPeticio": "9f2a1c48-3c7e-4a1b-9c62-1d0f8b4a77e1"
}
}Què fa útil aquesta resposta: dona tots els errors alhora (un viatge en comptes de tres), el camp exacte amb ruta completa en estructures imbricades (entrades.0.quantitat), un motiu llegible escrit per tu a l'esquema, un tipus estable (too_big, invalid_type) que un client pot tractar per programa sense dependre de l'idioma, i l'identificador de petició de 06-04 per creuar-lo amb els registres del servidor.
I què no hi ha d'aparèixer mai:
| No revelis | Per què |
|---|---|
| La traça de l'excepció | Delata rutes del sistema de fitxers i versions de biblioteques |
| El valor rebut tal qual | Pot contenir contrasenyes o dades personals, i acaba als registres |
| Noms de taules, columnes, fitxers o consultes internes | Mapa de franc de la teva infraestructura i ajuda per al següent intent |
| Si un correu existeix o no al sistema | Permet enumerar usuaris (crític al mòdul 8) |
400 enfront de 422
Tots dos són legítims i hi ha debat. La nostra política, coherent amb la taula ESTAT_PER_CODI del mòdul 4:
| Situació | Codi de domini | Estat |
|---|---|---|
| JSON malformat (ni tan sols es pot analitzar) | JSON_INVALID |
400 |
| Falta un camp o té el tipus incorrecte | DADES_INVALIDES |
400 |
| Format correcte però regla d'entrada violada (més de 6 entrades) | LIMIT_PER_COMANDA |
422 |
| Format correcte però l'estat no ho permet (aforament insuficient) | AFORAMENT_INSUFICIENT |
409 |
L'important no és quin tries, sinó que sigui coherent a tota l'API i estigui documentat.
- Usos menys obvis: respostes i configuració
Els esquemes no són només per al que entra.
Validar la configuració d'arrencada
A 06-02 vas escriure funcions a mà (obligatoria, enter, llista); un esquema fa el mateix més curt i amb millors missatges:
// src/config/esquema.js
const esquemaConfiguracio = z.object({
NODE_ENV: z.enum(['desenvolupament', 'proves', 'produccio']).default('desenvolupament'),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
LIMIT_COS: z.string().regex(/^\d+(kb|mb)$/i).default('100kb'),
CONFIAR_EN_PROXY: z.coerce.number().int().min(0).max(10).default(0),
ORIGENS_PERMESOS: z.string().default('')
.transform((v) => v.split(',').map((o) => o.trim()).filter(Boolean)),
});El mateix principi de «fallar ràpid», amb un únic lloc que descriu tot el que l'aplicació necessita per arrencar.
Validar la resposta d'un servei extern
src/serveis/canvi-divises.js fa fetch a una API que no controles: que avui retorni { rates: { USD: 1.08 } } no garanteix que demà no retorni { data: [...] } després d'un canvi de versió.
// src/serveis/canvi-divises.js (fragment)
const esquemaRespostaTipus = z.object({
base: z.literal('EUR'),
rates: z.record(z.string().length(3), z.number().positive()),
});
async function obtenirTipusCanvi(moneda) {
const resposta = await fetch(URL_TIPUS, { signal: AbortSignal.timeout(3000) });
const resultat = esquemaRespostaTipus.safeParse(await resposta.json());
if (!resultat.success) {
// Millor fallar amb un codi clar que propagar undefined per tot el sistema.
const missatge = 'La API de divises ha retornat un format inesperat';
throw Object.assign(new Error(missatge), { codi: 'SERVEI_EXTERN_CAIGUT' });
}
return resultat.data.rates[moneda];
}El cost és mínim i evita el pitjor tipus de fallada: la que no esclata on passa, sinó tres capes més avall amb un undefined incomprensible.
- Sanejament enfront de validació
Es confonen constantment, però són coses diferents:
| Validació | Sanejament | |
|---|---|---|
| Pregunta | És acceptable aquesta dada? | Com la faig inofensiva en aquest context? |
| Moment i resultat | En entrar; rebuig amb 400 | En sortir cap a una destinació; transformació del valor |
| Exemple | comentari de màxim 280 caràcters |
Escapar < i > en pintar-lo en HTML |
La regla que evita la majoria dels errors: valida en entrar, escapa en sortir. Per què no escapar en desar? Perquè l'escapament depèn de la destinació i desant escapat perds la dada original: si deses <script> i l'envies per JSON a una app mòbil, aquesta app mostra literalment <script>; si el mateix comentari va a un HTML, a un CSV i a un correu, cada destinació necessita un escapament diferent; i si canvies de sistema de plantilles, les teves dades ja estan contaminades amb l'escapament de l'anterior.
Desa la dada tal com és (validada, normalitzada) i escapa al punt de renderització. A Escena Viva, el public/app.js que pinta comentaris ha de fer servir textContent en comptes d'innerHTML: aquest és l'escapament correcte per a aquella destinació.
La menció honesta sobre la injecció
Veuràs repetit que «validar prevé la injecció SQL». És fals com a defensa principal: una llista negra de paraules (DROP, --, ;) s'esquiva de mil maneres i rebutja comentaris legítims de compradors que es diuen O'Brien. La protecció real són les consultes parametritzades, que separen el codi de les dades perquè el motor no interpreti mai un valor com a instrucció; això arriba al mòdul 7, amb Mongoose i Sequelize.
Mentrestant, a Escena Viva la persistència és un JSON i el risc equivalent és diferent però real:
- Recorregut de directoris si un identificador acaba formant part d'una ruta de fitxer. Per això
resoldreDinsDecontinua sent obligatori (mòdul 3). - Contaminació de prototip si fas
Object.assign({}, req.body)amb una clau__proto__. zod et protegeix perquèz.object()descarta el no declarat. - Denegació de servei amb cossos enormes o regex catastròfiques. Per això
limitaexpress.json()i patrons acotats als esquemes.
Validar redueix superfície, però no substitueix la defensa específica de cada destinació.
Errors Comuns i Consells
- Confiar en la validació del formulari o reassignar
req.query/req.params(TypeErrora Express 5): l'HTML és cortesia, el servidor és la frontera real, i el resultat va areq.dadesValidades. - Validar dins del domini.
Sessio.vendre()no ha de comprovar tipus: quan hi arriba ja són correctes. La seva feina és l'aforament. - Retornar només el primer error (obliga el client a un viatge per camp;
safeParseels dona tots) o no filtrar l'error intern: no enviïs mai traces ni valors rebuts, registra'ls al servidor i retorna el just. - Coerció indiscriminada (
z.coerce.number()converteix''en0itrueen1) o oblidar.trim()(un' ses-001-1 'falla contra la regex amb un missatge desconcertant). - Duplicar els esquemes entre rutes. Extreu els fragments comuns (
idSessio,correuComprador) i composa'ls. - Consell: l'esquema és documentació executable. Quan algú pregunti què accepta
POST /api/comandes, la resposta éssrc/esquemes/comanda.js, i no pot estar desactualitzada perquè és el codi que s'executa.
Exercicis
Exercici 1: esquema de comanda amb diverses sessions
Escena Viva vol permetre comandes amb entrades per a diverses sessions alhora. Escriu esquemaCrearComandaMultiple amb correu, canal i entrades, un array d'1 a 4 elements amb sessioId i quantitat. Afegeix-hi dues regles que un esquema de camp aïllat no pot expressar: el total d'entrades no pot passar de 6, i no es pot repetir la mateixa sessioId. Pista: .superRefine() sobre l'objecte complet.
Exercici 2: validar la query d'esdeveniments de punta a punta
Aplica validar(esquemaConsultaEsdeveniments, 'query') a GET /api/esdeveniments i adapta el controlador perquè llegeixi de req.dadesValidades.query. Comprova amb curl que ?perPagina=999 i ?pagina=abc retornen 400 amb el camp i el motiu, que ?exhaurit=true arriba com a booleà i que sense query els valors per defecte són pagina: 1, perPagina: 10.
Exercici 3: el camp de més
Envia a POST /api/comandes un cos amb un camp no declarat, per exemple {"sessioId":"ses-002-1","quantitat":2,"correu":"[email protected]","preuCentims":1}. Comprova que la comanda es crea i que preuCentims no arriba al servei. Després modifica l'esquema amb .strict() i comprova que ara retorna 400 indicant la clau sobrera. Raona en quins casos preferiries cada comportament.
Solucions
Solució 1
// src/esquemes/comanda.js (ampliacio)
const esquemaLiniaComanda = z.object({ sessioId: idSessio, quantitat: quantitatEntrades });
const esquemaCrearComandaMultiple = z
.object({
correu: correuComprador,
canal: canalVenda.default('web'),
entrades: z.array(esquemaLiniaComanda).min(1, 'Almenys una linia').max(4, 'Maxim 4 sessions'),
})
.superRefine((comanda, context) => {
// Regla 1: total d'entrades de la comanda.
const total = comanda.entrades.reduce((suma, linia) => suma + linia.quantitat, 0);
if (total > LIMIT_ENTRADES_PER_COMANDA) {
context.addIssue({
code: 'custom', path: ['entrades'],
message: `La comanda suma ${total} entrades i el maxim es ${LIMIT_ENTRADES_PER_COMANDA}`,
});
}
// Regla 2: sense sessions repetides.
const vistes = new Set();
comanda.entrades.forEach((linia, i) => {
if (vistes.has(linia.sessioId)) {
context.addIssue({
code: 'custom', path: ['entrades', i, 'sessioId'],
message: `La sessio ${linia.sessioId} es repeteix; agrupa les quantitats`,
});
}
vistes.add(linia.sessioId);
});
});superRefine és el lloc de les regles que creuen camps. Compte: continuen sent regles de forma, no d'estat. «Hi ha aforament per a aquestes 6 entrades?» continua sent del domini, perquè depèn de les vendes del moment.
Solució 2
// src/controladors/esdeveniments.js
async function llistarEsdeveniments(peticio, resposta) {
const { sala, exhaurit, pagina, perPagina } = peticio.dadesValidades.query;
let esdeveniments = await obtenirCataleg();
if (sala) esdeveniments = esdeveniments.filter((e) => e.sala === sala);
if (exhaurit !== undefined) esdeveniments = esdeveniments.filter((e) => e.exhaurit === exhaurit);
const total = esdeveniments.length;
const desDe = (pagina - 1) * perPagina;
resposta.json({
pagina, perPagina, total,
totalPagines: Math.max(1, Math.ceil(total / perPagina)),
esdeveniments: esdeveniments.slice(desDe, desDe + perPagina).map((e) => e.toJSON()),
});
}# ?perPagina=999 i ?pagina=abc retornen 400 DADES_INVALIDES amb el camp i el motiu:
curl -s 'localhost:3000/api/esdeveniments?perPagina=999' | head -c 200
# {"error":{"codi":"DADES_INVALIDES",...,"detalls":[{"camp":"perPagina",...,"tipus":"too_big"}]}}
curl -s 'localhost:3000/api/esdeveniments' | head -c 60 # sense query, valors per defecte
# {"pagina":1,"perPagina":10,"total":3,"totalPagines":1,...Compara el controlador amb la versió de l'exercici de 06-03: han desaparegut els parseInt, els ?? i els topalls manuals. Aquesta lògica no s'ha perdut, s'ha mudat a l'esquema.
Solució 3
# Amb z.object() normal: el camp de mes es descarta en silenci.
curl -s -X POST localhost:3000/api/comandes -H 'Content-Type: application/json' \
-d '{"sessioId":"ses-002-1","quantitat":2,"correu":"[email protected]","preuCentims":1}'
# 201: la comanda es crea amb el preu real de la sessio, mai amb l'enviat.
# Amb .strict() a l'esquema, es rebutja amb 400 DADES_INVALIDES i detall
# {"camp":"preuCentims","tipus":"unrecognized_keys"}.Quan preferir cadascun: podar (el comportament per defecte) encaixa en una API pública amb clients variats, perquè un client antic que envia camps obsolets continua funcionant; .strict() encaixa en una API interna o amb camps sensibles, perquè un camp inesperat sol ser un error del client i convé avisar aviat. El crucial en tots dos casos: preuCentims no ha de sortir mai del cos del client; el preu el posa el servidor llegint la sessió. Si un camp del cos pot alterar els diners, tens un problema molt més greu que un esquema mal configurat.
Conclusió
La validació és la frontera de la teva aplicació, i ara Escena Viva la té ben posada. Saps per què el formulari del navegador no compta, què es valida (cos, paràmetres, query, capçaleres) i —el més important conceptualment— on viu cada cosa: la vora garanteix la forma, el domini garanteix les regles que depenen de l'estat; la primera retorna 400, el segon 409. Has canviat les cinquanta línies imperatives del mòdul 4 per esquemes declaratius de zod que són alhora validació, conversió, normalització, poda de camps sobrers i documentació executable. El middleware validar(esquema, origen) es col·loca a qualsevol ruta, deixa el resultat a req.dadesValidades respectant que req.query sigui de només lectura a Express 5, i allibera els controladors de tota comprovació. I has vist els usos menys obvis —validar la configuració d'arrencada i les respostes de serveis externs— a més de la distinció entre validar en entrar i escapar en sortir, amb l'advertiment honest que la defensa real contra la injecció són les consultes parametritzades del mòdul 7.
Queda un detall sense resoldre, i és el que tanca el mòdul: el middleware validar no respon, crida seguent(new ErrorDeValidacio(...)), i aquest ErrorDeValidacio encara no existeix ni ningú no el recull. El mateix passa amb l'ESDEVENIMENT_NO_TROBAT que propaga carregarEsdeveniment, amb l'AFORAMENT_INSUFICIENT que llança el domini, amb el JSON malformat que rebutja express.json() i amb el 404 de les rutes inexistents.
A la lliçó següent, Gestió d'Errors, construïm la destinació de tots ells: el middleware d'error de quatre arguments, una jerarquia pròpia a src/errors.js, el lloc definitiu de la taula ESTAT_PER_CODI que vas escriure al mòdul 4, la distinció entre errors operatius i errors de programació, i la política de què fer quan alguna cosa falla fora de l'abast d'Express.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
