Tancàvem el mòdul 2 amb el contracte de la Botiga Aroma complet sobre el paper: 24 URIs, mètodes amb la seva idempotència resolta, catàleg d'errors, representacions JSON, paginació, versionat i documentació. Ni una línia de servidor. Aquest mòdul compleix aquell contracte amb Node.js 20 i Express, i com en qualsevol obra seriosa, es comença pels fonaments: preparar la màquina, crear el projecte, triar i entendre cada dependència, definir l'estructura de carpetes que sostindrà vuit lliçons de codi i aïllar la configuració en variables d'entorn. És la lliçó menys vistosa del mòdul i la que més problemes evita: gairebé tots els embussos d'un principiant amb Node vénen d'una versió equivocada, d'un import que el projecte no admet o d'un secret escrit a foc dins del codi. En acabar tindràs l'esquelet del projecte a punt perquè la lliçó següent l'arrenqui; encara no hi haurà servidor, i això és intencionat.
Contingut
- Què construirem i amb què
- Node.js 20 LTS: instal·lació i verificació
- Gestors de versions:
nvmifnm - npm i
npx - Creació del projecte amb
npm init - Anatomia de
package.json - ESM davant de CommonJS i
"type": "module" - Versionat semàntic a les dependències:
^i~ package-lock.json,npm cidavant denpm installdependenciesdavant dedevDependencies- Les dependències del curs, una per una
- Els scripts npm del projecte
- L'estructura de carpetes
- Configuració amb variables d'entorn
.gitignorei inicialització de Git- Editor i eines de treball
- Comprovació final de l'entorn
- Què construirem i amb què
Durant les vuit lliçons d'aquest mòdul construirem un únic projecte que creix. No hi haurà exemples solts que es llencin a les escombraries: cada lliçó parteix de l'estat en què la va deixar l'anterior i diu explícitament quins fitxers crea i quins modifica.
| Lliçó | Què afegeix al projecte |
|---|---|
| 03-01 | Esquelet: package.json, carpetes, configuració, Git |
| 03-02 | Servidor Express, router /v1, primeres rutes de cafès en memòria |
| 03-03 | Controladors, serveis, mapejadors, CRUD complet, filtres i paginació |
| 03-04 | Esquemes Zod i middleware de validació |
| 03-05 | SQLite amb el patró repositori, migracions, transaccions |
| 03-06 | Registre, inici de sessió, JWT, rols i permisos |
| 03-07 | ErrorApi i middleware d'errors unificat |
| 03-08 | Proves unitàries i d'integració |
La pila tècnica queda fixada aquí i no canvia:
| Peça | Elecció | Per què |
|---|---|---|
| Execució | Node.js 20 LTS | Suport a llarg termini, node --watch i node:test integrats |
| Mòduls | ESM (import/export) |
És l'estàndard de JavaScript; CommonJS és el llegat |
| Framework HTTP | Express 4.x | Minimalista, explícit, el més estès; res de màgia oculta |
| Validació | Zod | Esquemes declaratius amb inferència de tipus |
| Persistència | SQLite via better-sqlite3 |
Zero configuració, SQL real, substituïble per PostgreSQL |
| Autenticació | JWT (jsonwebtoken) + bcrypt |
Sense estat, encaixa amb REST |
| Proves | node:test + Supertest |
Sense dependències extra per al runner |
Una nota sobre l'idioma: com vam fixar a la guia d'estil de 02-01, el codi i els comentaris van en català. Hi veuràs obtenirCafes, preuEuros, repositoriCafes o gestorErrors, i els fitxers del projecte s'anomenen rutes/cafes.js o serveis/comandes.js. Només mantenen el nom en anglès els que imposa l'eina: package.json, .env, node_modules.
- Node.js 20 LTS: instal·lació i verificació
Node.js és l'entorn que executa JavaScript fora del navegador. Les versions parelles (18, 20, 22) són LTS (Long Term Support): reben correccions durant uns tres anys i són les que s'usen en producció. Les senars són experimentals.
El primer és veure què hi ha instal·lat:
Si node --version respon v20.x.x, ja tens el que cal. Si respon v16.x.x o l'ordre no existeix, continua llegint. Necessitem 20 o superior per tres motius concrets que farem servir en aquest mòdul:
node --watch: recàrrega automàtica del servidor en desar, sensenodemon.node:testinode --test: runner de proves integrat (03-08).node --env-file: càrrega de fitxers.envsense llibreria (des de la 20.6).
- Gestors de versions:
nvm i fnm
nvm i fnmPodries instal·lar Node des de nodejs.org i acabar. No ho facis. Un instal·lador deixa una única versió global, i tan bon punt treballis en dos projectes —un en Node 18 i un altre en Node 20— tindràs un problema que es resol desinstal·lant i reinstal·lant. Un gestor de versions permet tenir-ne diverses alhora i canviar d'una a l'altra en segons, fins i tot per carpeta.
Els dos habituals:
| Gestor | Escrit en | Avantatge | Plataformes |
|---|---|---|---|
| nvm | Bash | El més estès, moltíssima documentació | Linux, macOS (Windows: nvm-windows, projecte diferent) |
| fnm | Rust | Molt més ràpid, canvi automàtic per carpeta | Linux, macOS, Windows natiu |
Instal·lació d'nvm a Linux o macOS:
# Descarrega i instal·la nvm (revisa la darrera versió al seu repositori)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Recarrega la configuració del shell perquè 'nvm' estigui disponible
source ~/.bashrc # o ~/.zshrc si uses zshÚs diari:
nvm install 20 # Instal·la la darrera 20.x LTS
nvm use 20 # Usa la 20 en aquest terminal
nvm alias default 20 # La 20 serà la versió per defecte en obrir un terminal nou
nvm ls # Llista les versions instal·ladesUn detall molt pràctic: si crees un fitxer .nvmrc a l'arrel del projecte amb el contingut 20, qualsevol que cloni el repositori pot executar nvm use i obtenir la versió correcta sense preguntar. L'afegim:
Amb fnm les ordres són gairebé idèntiques (fnm install 20, fnm use 20) i, a més, llegeix el .nvmrc automàticament en entrar a la carpeta si el configures amb --use-on-cd.
- npm i
npx
npxEn instal·lar Node vénen dues ordres que convé no confondre:
| Ordre | Què fa | Exemple |
|---|---|---|
npm |
Gestor de paquets: instal·la, actualitza i executa scripts | npm install express |
npx |
Executa un paquet sense instal·lar-lo permanentment | npx eslint src/ |
npx és especialment útil per a eines d'un sol ús (generadors, migradors) i per executar binaris que són a node_modules/.bin sense escriure el camí complet.
Existeixen alternatives a npm —pnpm (més ràpid i estalvia disc), yarn, bun—, i són perfectament vàlides. Aquest curs fa servir npm perquè ve amb Node i no afegeix un requisit més.
- Creació del projecte amb
npm init
npm initCreem la carpeta i la inicialitzem:
mkdircrea el directori del projecte. El nom en kebab-case és la convenció d'npm.npm init -ygenera unpackage.jsonamb valors per defecte sense fer preguntes. Sense-y, npm pregunta nom, versió, descripció, etc. de manera interactiva.
El resultat és un package.json mínim:
{
"name": "botiga-aroma-api",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
- Anatomia de
package.json
package.jsonpackage.json és la fitxa d'identitat del projecte: qui és, què necessita per funcionar i com s'executa. El substituirem per la versió definitiva del curs, camp a camp:
{
"name": "botiga-aroma-api",
"version": "1.0.0",
"description": "API RESTful de la Botiga Aroma, botiga de cafè d'especialitat",
"type": "module",
"main": "src/servidor.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "node --watch src/servidor.js",
"start": "node src/servidor.js",
"test": "node --test proves/",
"lint": "eslint src/ proves/",
"format": "prettier --write \"**/*.{js,json,md}\""
},
"license": "UNLICENSED",
"private": true
}Què significa cada camp:
| Camp | Per a què serveix |
|---|---|
name |
Identificador del paquet. En minúscules, sense espais |
version |
Versió del projecte en format SemVer (02-07). Compte: no és la versió de l'API, que va a la ruta /v1 |
description |
Text lliure; apareix al registre d'npm si es publica |
type |
"module" activa ESM. És la decisió de la secció següent |
main |
Punt d'entrada si un altre paquet importa aquest |
engines |
Versions de Node admeses. npm avisa si no coincideixen |
scripts |
Ordres abreujades que es llancen amb npm run <nom> |
license |
UNLICENSED per a codi privat; MIT o similar per a codi obert |
private |
true impedeix publicar-lo a npm per accident. Imprescindible en codi d'empresa |
- ESM davant de CommonJS i
"type": "module"
"type": "module"Node arrossega dos sistemes de mòduls i cal triar-ne un conscientment, perquè barrejar-los és la primera font d'errors incomprensibles.
| Aspecte | CommonJS (el llegat) | ESM (l'estàndard) |
|---|---|---|
| Importar | const express = require('express') |
import express from 'express' |
| Exportar | module.exports = alguna cosa |
export default alguna cosa / export { algunaCosa } |
| Quan es resol | En temps d'execució | En temps d'anàlisi (estàtic) |
| Activació | Per defecte | "type": "module" o extensió .mjs |
| Extensió a les rutes pròpies | Opcional (./cafes) |
Obligatòria (./cafes.js) |
__dirname, __filename |
Disponibles | No existeixen (hi ha equivalents) |
Top-level await |
No | Sí |
La Botiga Aroma fa servir ESM. És l'estàndard del llenguatge, funciona igual al navegador i al servidor, i permet await al nivell superior d'un fitxer, cosa que agrairem en obrir la base de dades a 03-05.
Les dues conseqüències pràctiques que més despisten al principi:
// CORRECTE en ESM: l'extensió .js és obligatòria a les rutes pròpies
import { repositoriCafes } from './repositoris/cafes-memoria.js';
// INCORRECTE en ESM: falta l'extensió → ERR_MODULE_NOT_FOUND
import { repositoriCafes } from './repositoris/cafes-memoria';
// Els paquets de node_modules NO porten extensió
import express from 'express';// __dirname no existeix en ESM. Equivalent:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const rutaFitxer = fileURLToPath(import.meta.url); // Camí absolut d'aquest fitxer
const rutaCarpeta = dirname(rutaFitxer); // La seva carpeta contenidoraFixa't també en el prefix node: (node:url, node:path, node:fs). És la manera moderna i explícita d'importar mòduls interns de Node, i evita que un paquet maliciós anomenat path a npm suplanti el mòdul del sistema. Fes-lo servir sempre.
- Versionat semàntic a les dependències:
^ i ~
^ i ~Quan instal·les un paquet, npm escriu a package.json un rang de versions acceptables, no una versió exacta. Recorda SemVer de 02-07: MAJOR.MENOR.PEDAÇ.
| Rang | Vol dir | Accepta | No accepta |
|---|---|---|---|
^4.18.2 |
Compatible: fixa la major | 4.18.3, 4.19.0 |
5.0.0 |
~4.18.2 |
Aproximat: fixa major i menor | 4.18.3, 4.18.9 |
4.19.0 |
4.18.2 |
Exacta | només 4.18.2 |
qualsevol altra |
* o latest |
Qualsevol | tot | — |
El valor per defecte d'npm és ^, i és un compromís raonable: reps correccions d'errors i funcionalitats noves sense canvis trencadors, sempre que l'autor respecti SemVer. No facis servir mai *: significa "instal·la'm el que sigui", i un dia el teu build es trencarà sense que hagis tocat res.
package-lock.json, npm ci davant de npm install
package-lock.json, npm ci davant de npm installSi package.json diu ^4.18.2, dues persones que instal·lin en dates diferents poden acabar amb 4.18.2 i 4.19.1. Això és exactament el que produeix el clàssic "a la meva màquina funciona". La solució és package-lock.json: un fitxer generat automàticament que registra la versió exacta de cada paquet instal·lat i de cada dependència de les seves dependències, amb el seu hash d'integritat.
Regles d'or:
package-lock.jsones versiona a Git. Sempre. No és un fitxer temporal.- No s'edita a mà mai.
I d'aquí ve la diferència entre les dues ordres d'instal·lació:
npm install |
npm ci |
|
|---|---|---|
| Llegeix | package.json (i actualitza el lock) |
Només package-lock.json |
| Pot canviar versions | Sí | No, mai |
Esborra node_modules abans |
No | Sí, sencer |
| Velocitat | Menor | Major |
| Ús recomanat | Desenvolupament, en afegir paquets | Integració contínua i producció |
Regla pràctica: al teu portàtil, npm install; al pipeline de CI i al servidor, npm ci (ho veurem a 05-05). Si npm ci falla perquè el lock no concorda amb package.json, això és una virtut, no una fallada: t'està avisant que algú ha tocat les dependències sense regenerar el lock.
dependencies davant de devDependencies
dependencies davant de devDependenciesnpm install express # va a "dependencies"
npm install --save-dev eslint # va a "devDependencies" (abreujat: -D)| Bloc | Conté | S'instal·la en producció? |
|---|---|---|
dependencies |
El que el codi necessita per executar-se | Sí |
devDependencies |
Eines de desenvolupament: proves, linters, formatadors | No (npm ci --omit=dev) |
La distinció no és cosmètica: redueix la mida de la imatge de desplegament i, sobretot, la superfície d'atac. Un linter no hauria d'existir ni tan sols al servidor de producció. L'error invers —posar a devDependencies alguna cosa que el servidor fa servir en temps d'execució— produeix un ERR_MODULE_NOT_FOUND que només apareix en desplegar.
- Les dependències del curs, una per una
Instal·lem primer les de producció:
| Paquet | Què fa | On el farem servir |
|---|---|---|
express |
Framework HTTP: encaminament, middleware, utilitats de petició i resposta | 03-02 endavant |
zod |
Validació per esquemes declaratius amb inferència de tipus | 03-04 |
better-sqlite3 |
Client SQLite síncron, molt ràpid, sense servidor de base de dades | 03-05 |
jsonwebtoken |
Signatura i verificació de JWT | 03-06 |
bcrypt |
Hash de contrasenyes amb sal i cost configurable | 03-06 |
dotenv |
Carrega variables d'un fitxer .env a process.env |
ara mateix |
I les de desenvolupament:
| Paquet | Què fa | On el farem servir |
|---|---|---|
supertest |
Llança peticions HTTP contra l'app d'Express sense obrir port | 03-08 |
eslint |
Detecta errors i mals usos abans d'executar | continu |
prettier |
Formata el codi de manera consistent i automàtica | continu |
eslint-config-prettier |
Desactiva les regles d'ESLint que xoquen amb Prettier | continu |
Dos aclariments sobre eleccions que criden l'atenció:
express@4i no la 5. Express 5 ja és estable, però la immensa majoria del codi, la documentació i les respostes que trobaràs són de la 4. A més, la 4 té una mancança molt didàctica —no captura els errors de funcionsasync— que ens obliga a entendre de debò la gestió d'errors a 03-07. Direm al seu moment què canvia amb la 5.bcrypti nobcryptjs.bcryptés una extensió nativa (es compila en instal·lar-la) i és més ràpida. Si la seva compilació falla a la teva màquina —sol passar a Windows sense eines de compilació—,bcryptjsés un substitut en JavaScript pur amb la mateixa API:npm install bcryptjsi canvia l'import.
No necessitem nodemon: node --watch fa el mateix des de Node 18. Ni cors, helmet o express-rate-limit, que arribaran al mòdul 4 quan toqui endurir l'API.
- Els scripts npm del projecte
Els scripts són la interfície de què disposa qualsevol que arribi al repositori. El primer que fa un desenvolupador nou és mirar scripts per saber com s'arrenca el projecte.
"scripts": {
"dev": "node --watch src/servidor.js",
"start": "node src/servidor.js",
"test": "node --test proves/",
"lint": "eslint src/ proves/",
"format": "prettier --write \"**/*.{js,json,md}\""
}| Script | Ordre | Què fa |
|---|---|---|
npm run dev |
node --watch |
Arrenca i es reinicia sol en desar un fitxer |
npm start |
node |
Arrenca sense vigilància. És el de producció |
npm test |
node --test |
Executa totes les proves de proves/ |
npm run lint |
eslint |
Analitza el codi a la recerca d'errors |
npm run format |
prettier --write |
Reformata tots els fitxers |
Detall d'npm que confon: start i test són scripts "coneguts" i s'invoquen sense run (npm start, npm test); la resta necessita run (npm run dev). Tots dos funcionen amb run, així que en cas de dubte, escriu npm run.
- L'estructura de carpetes
Aquí hi ha la decisió d'arquitectura de tot el mòdul. Organitzarem el codi per capes, amb una responsabilitat clara per carpeta:
mkdir -p src/{rutes,controladors,serveis,repositoris,esquemes,middleware,config,errors}
mkdir -p proves/{unitaries,integracio,ajudes}
mkdir -p migracions| Carpeta / fitxer | Responsabilitat | Lliçó |
|---|---|---|
src/servidor.js |
Arrenca el procés: llegeix el port i crida listen() |
03-02 |
src/app.js |
Construeix l'aplicació Express i munta els middleware | 03-02 |
src/rutes/ |
Declara quina URI i mètode invoca quin controlador | 03-02 |
src/controladors/ |
Tradueix HTTP ↔ domini: llegeix req, crida el servei, escriu res |
03-03 |
src/serveis/ |
Lògica de negoci. No sap que existeix HTTP | 03-03 |
src/repositoris/ |
Accés a dades. L'únic que sap de la base de dades | 03-03 / 03-05 |
src/esquemes/ |
Esquemes Zod de validació d'entrada | 03-04 |
src/middleware/ |
Peces transversals: validació, autenticació, errors | 03-04 endavant |
src/config/ |
Lectura i validació de la configuració de l'entorn | 03-01 |
src/errors/ |
ErrorApi i les seves fàbriques |
03-07 |
migracions/ |
Fitxers .sql versionats que creen l'esquema |
03-05 |
proves/ |
Proves unitàries, d'integració i utilitats de suport | 03-08 |
Per què tantes carpetes per a una API petita? Perquè cada frontera resol un problema real, i totes es cobren el seu benefici dins d'aquest mateix mòdul:
- Rutes separades dels controladors: el mapa d'URIs de 02-02 es llegeix d'una ullada en un fitxer, sense lògica pel mig.
- Controladors separats dels serveis: el servei no toca
reqnires, així que es pot provar sense aixecar un servidor (03-08) i reutilitzar des d'un script de línia d'ordres o una tasca programada. - Serveis separats dels repositoris: a 03-05 substituïm el magatzem en memòria per SQLite sense tocar ni una sola línia dels controladors ni dels serveis. Aquesta és la prova que la frontera val la pena.
- Middleware a part: validació, autenticació i errors són transversals; si viuen dins de les rutes, acaben duplicats en vint llocs.
El flux d'una petició, de fora cap a dins, serà sempre el mateix:
graph LR C[Client] --> R[rutes/] R --> M[middleware/] M --> CT[controladors/] CT --> S[serveis/] S --> RP[repositoris/] RP --> BD[(Dades)]
I la regla que ho manté sa: les fletxes mai no van cap enrere. Un repositori no crida un servei, i un servei no importa res d'Express.
- Configuració amb variables d'entorn
El port, el camí de la base de dades i el secret de signatura dels JWT no poden estar escrits al codi. Canvien entre el teu portàtil, l'entorn de proves i producció, i alguns són secrets.
L'estàndard de facto és la metodologia Twelve-Factor App: la configuració viu a l'entorn, no al codi. A Node es llegeix amb process.env.
Creem el fitxer .env a l'arrel:
# .env — configuració local. NO es puja a Git.
NODE_ENV=desenvolupament
PORT=3000
BASE_URL=http://localhost:3000
RUTA_BASE_DADES=./dades/aroma.db
JWT_SECRET=canvia-aixo-per-una-cadena-llarga-i-aleatoria-en-produccio
JWT_CADUCITAT=1hI .env.example, que sí que es versiona, amb les mateixes claus però sense valors reals:
# .env.example — plantilla. Copia-la a .env i omple els valors.
NODE_ENV=desenvolupament
PORT=3000
BASE_URL=http://localhost:3000
RUTA_BASE_DADES=./dades/aroma.db
JWT_SECRET=
JWT_CADUCITAT=1hAquest segon fitxer és documentació executable: qui cloni el repositori fa cp .env.example .env, l'omple i arrenca. Sense ell, l'única manera de saber quines variables calen és llegir tot el codi o esperar que peti.
14.1. src/config/entorn.js
Llegir process.env.PORT dispers per tot el codi és mala idea: no saps quines variables existeixen, no hi ha valors per defecte centralitzats i un error tipogràfic produeix un undefined silenciós. Centralitzem la lectura en un únic mòdul que, a més, valida en arrencar i falla sorollosament si hi falta alguna cosa:
// src/config/entorn.js
import 'dotenv/config';
/**
* Llegeix una variable obligatòria. Si no existeix, avorta l'arrencada.
* Fallar en arrencar és molt millor que fallar a la petició número 500.
*/
function obligatoria(nom) {
const valor = process.env[nom];
if (valor === undefined || valor.trim() === '') {
throw new Error(`Falta la variable d'entorn obligatòria: ${nom}`);
}
return valor;
}
/** Llegeix una variable opcional amb valor per defecte. */
function opcional(nom, perDefecte) {
const valor = process.env[nom];
return valor === undefined || valor.trim() === '' ? perDefecte : valor;
}
/** Llegeix una variable numèrica i comprova que de veritat ho és. */
function numerica(nom, perDefecte) {
const valor = opcional(nom, String(perDefecte));
const numero = Number(valor);
if (!Number.isInteger(numero)) {
throw new Error(`La variable ${nom} ha de ser un nombre enter, i val "${valor}"`);
}
return numero;
}
export const entorn = {
nodeEnv: opcional('NODE_ENV', 'desenvolupament'),
port: numerica('PORT', 3000),
baseUrl: opcional('BASE_URL', 'http://localhost:3000'),
rutaBaseDades: opcional('RUTA_BASE_DADES', './dades/aroma.db'),
jwtSecret: obligatoria('JWT_SECRET'),
jwtCaducitat: opcional('JWT_CADUCITAT', '1h'),
};
// Congelem l'objecte perquè cap part del codi el pugui modificar
// en calent: la configuració es llegeix un cop i no canvia durant l'execució.
Object.freeze(entorn);Línia a línia, el que importa:
import 'dotenv/config'executa dotenv pel seu efecte secundari: llegeix.envi n'aboca les claus aprocess.env. Ha de passar abans de llegir cap variable, i per això és a la primera línia del primer mòdul que s'importa.obligatoria()llança un error si la variable falta. Això és deliberat: preferim que el procés no arrenqui a que arrenqui ambjwtSecret === undefinedi signi tokens insegurs durant setmanes.numerica()converteix i comprova. Recorda que totes les variables d'entorn són cadenes de text:process.env.PORTés"3000", no3000.Object.freezeimpedeix reassignacions accidentals.
A partir d'ara, qualsevol fitxer que necessiti configuració fa import { entorn } from '../config/entorn.js' i utilitza entorn.port. Ningú més toca process.env.
A 03-04 coneixerem Zod i veuràs que aquest fitxer es podria escriure amb un esquema de cinc línies. El deixem en JavaScript pur a propòsit: la configuració es valida abans que existeixi res més, i convé que no depengui de tercers.
Com a curiositat útil: des de Node 20.6 existeix node --env-file=.env src/servidor.js, que fa la feina de dotenv sense instal·lar res. Mantenim dotenv perquè funciona igual en qualsevol versió i a les eines de proves.
.gitignore i inicialització de Git
.gitignore i inicialització de GitEl fitxer .env no es puja mai a Git. Un secret pujat a un repositori es considera compromès per sempre, encara que esborris el commit: queda a l'històric, als clons dels teus companys i a les memòries cau de la plataforma. Rotar-lo és l'única solució, i és molt més car que escriure bé el .gitignore.
# .gitignore
# Dependències
node_modules/
# Configuració local i secrets
.env
.env.*.local
# Base de dades local i els seus fitxers auxiliars
dades/
*.db
*.db-journal
# Registres i cobertura
*.log
coverage/
# Sistema operatiu i editors
.DS_Store
.vscode/*
!.vscode/extensions.jsonObserva que .env.example no està ignorat (només ho està .env), que és justament el que volem. I que package-lock.json tampoc: és un fitxer que ha de viatjar amb el projecte.
Inicialitzem el repositori:
Abans de confirmar, verifica amb git status que no apareix .env a la llista de fitxers afegits. Si hi apareix, el .gitignore està malament o el fitxer ja estava indexat: git rm --cached .env el treu de l'índex sense esborrar-lo del disc.
- Editor i eines de treball
16.1. Editor
Qualsevol editor serveix, però VS Code és el més comú a l'ecosistema Node i té integració directa amb les eines del curs. Extensions recomanades: ESLint, Prettier - Code formatter i REST Client (permet llançar peticions des d'un fitxer .http, molt còmode per provar l'API sense sortir de l'editor).
16.2. ESLint
ESLint analitza el codi sense executar-lo i detecta variables sense fer servir, await oblidats o comparacions sospitoses. Configuració plana (la moderna, eslint.config.js):
// eslint.config.js
import js from '@eslint/js';
import configPrettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: {
process: 'readonly',
console: 'readonly',
},
},
rules: {
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'no-console': 'off',
eqeqeq: ['error', 'always'],
},
},
configPrettier,
];js.configs.recommendedactiva el conjunt de regles raonables que manté el mateix ESLint.sourceType: 'module'li diu a ESLint que el codi és ESM (coherent amb"type": "module").argsIgnorePattern: '^_'permet arguments sense fer servir si comencen per guió baix. En tindrem necessitat: el middleware d'errors d'Express obliga a declarar quatre paràmetres encara que no facis servir el darrer (03-07).eqeqeqobliga a===en lloc de==.configPrettierva l'últim i desactiva les regles d'estil que es trepitjarien amb Prettier.
Necessita un paquet més: npm install --save-dev @eslint/js.
16.3. Prettier
Prettier no opina sobre si el codi és correcte, només sobre com es veu, i elimina per sempre les discussions sobre cometes i comes:
Desa això com a .prettierrc.json. Amb l'extensió de VS Code i "Format on Save" activat, el format deixa de ser un tema de conversa.
16.4. Recàrrega automàtica
node --watch vigila els fitxers importats pel punt d'entrada i reinicia el procés en desar. No s'ha de confondre amb node --watch-path, que vigila una carpeta concreta, ni amb el hot reload del navegador: aquí el procés es reinicia sencer, així que l'estat en memòria es perd. A 03-02 i 03-03 els cafès viuen en memòria, i notaràs que un reinici els retorna al seu valor inicial. És normal i desapareix a 03-05 amb SQLite.
16.5. curl i Postman
Durant tot el mòdul provarem amb curl, que ja vam fer servir a 01-03. És universal, es copia i s'enganxa a qualsevol documentació i no amaga res:
-iinclou les capçaleres de resposta, imprescindible per comprovarLocation,LinkoAllow.-vmostra a més la petició completa.-ssilencia la barra de progrés, útil en encadenar ambjq.
Postman és un client gràfic amb col·leccions, entorns i proves automatitzades; és una eina excel·lent i li dediquem sencera la lliçó 05-01. Aquí no el necessitem.
- Comprovació final de l'entorn
El projecte encara no té servidor —això és 03-02—, però sí que podem verificar que els fonaments aguanten. Crea un fitxer temporal comprovar.js a l'arrel:
// comprovar.js — verificació de l'entorn. S'esborra en acabar la lliçó.
import { entorn } from './src/config/entorn.js';
console.log('Node.js:', process.version);
console.log('Entorn:', entorn.nodeEnv);
console.log('Port configurat:', entorn.port, typeof entorn.port);
console.log('Base de dades:', entorn.rutaBaseDades);
console.log('Hi ha secret JWT?:', entorn.jwtSecret ? 'sí' : 'no');Node.js: v20.11.1 Entorn: desenvolupament Port configurat: 3000 number Base de dades: ./dades/aroma.db Hi ha secret JWT?: sí
Fixa't en number: la conversió de numerica() ha funcionat. Ara prova el camí de fallada, que és igual d'important: comenta la línia JWT_SECRET= del teu .env i torna a executar.
El procés mor immediatament amb un missatge que diu exactament què falta. Aquest és el comportament correcte. Restaura el .env i esborra comprovar.js.
Estat del projecte en acabar la lliçó:
botiga-aroma-api/
├── .env (ignorat per Git)
├── .env.example
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── eslint.config.js
├── package.json
├── package-lock.json
├── node_modules/ (ignorat per Git)
├── migracions/ (buida, s'omple a 03-05)
├── proves/
│ ├── ajudes/
│ ├── integracio/
│ └── unitaries/
└── src/
├── config/
│ └── entorn.js
├── controladors/
├── errors/
├── esquemes/
├── middleware/
├── repositoris/
├── rutes/
└── serveis/Errors Comuns i Consells
1. ERR_REQUIRE_ESM o Cannot use import statement outside a module. Falta "type": "module" al package.json, o estàs fent servir require en un projecte ESM. Decideix un sistema i respecta'l a tot el projecte.
2. ERR_MODULE_NOT_FOUND amb un fitxer que sí que existeix. En ESM l'extensió .js és obligatòria a les importacions relatives. ./serveis/cafes falla; ./serveis/cafes.js funciona.
3. Pujar el .env a Git. L'error més car d'aquesta lliçó. Escriu el .gitignore abans del primer git add. Si ja ha passat, no n'hi ha prou amb esborrar el fitxer: cal rotar el secret.
4. npm install al servidor de producció. Pot instal·lar versions diferents de les que vas provar. Fes servir sempre npm ci, que respecta el lock al peu de la lletra.
5. Afegir package-lock.json al .gitignore. Es veu més del que sembla i anul·la tota la reproductibilitat. El lock es versiona.
6. Instal·lar globalment (npm install -g) les dependències del projecte. El que és global no queda registrat al package.json, així que funciona a la teva màquina i a cap altra. Només s'instal·len globalment eines de sistema, i sovint ni això: npx les executa sense instal·lar.
7. Llegir process.env des de mitja dotzena de fitxers. Centralitza-ho a config/entorn.js. El dia que una variable canviï de nom, tocaràs un lloc i no sis.
8. Confondre la versió del package.json amb la versió de l'API. "version": "1.0.0" és de l'artefacte de programari; /v1 és del contracte. Poden avançar per separat: 1.4.7 continua servint /v1 (02-07).
Consell: fes un commit al final de cada lliçó d'aquest mòdul. Si alguna cosa es trenca a 03-05, git diff et dirà en trenta segons què ha canviat respecte de l'estat bo.
Exercicis
Exercici 1
Afegeix a la configuració una variable nova, LIMIT_PAGINA_MAXIM, que fixi el màxim d'elements per pàgina decidit a 02-06. Ha de ser numèrica, opcional, amb valor per defecte 100, i l'arrencada ha de fallar si algú hi escriu un valor no enter o més gran que 1000. Modifica .env, .env.example i src/config/entorn.js.
Exercici 2
Un company clona el repositori i executa npm start. Obté:
Explica què ha passat exactament, per què és el comportament desitjable i quins dos passos ha de seguir. Després, proposa una millora del missatge d'error perquè sigui autoexplicatiu.
Exercici 3
Classifica aquests paquets en dependencies o devDependencies i justifica cadascun en una frase: express, supertest, dotenv, prettier, better-sqlite3, eslint, jsonwebtoken. Després indica què passaria exactament si dotenv acabés per error a devDependencies i es desplegués amb npm ci --omit=dev.
Solucions
Solució 1
A .env i .env.example:
A src/config/entorn.js, una funció nova i un camp més:
/** Llegeix una variable numèrica i comprova que és dins d'un rang. */
function numericaEnRang(nom, perDefecte, minim, maxim) {
const numero = numerica(nom, perDefecte);
if (numero < minim || numero > maxim) {
throw new Error(
`La variable ${nom} ha d'estar entre ${minim} i ${maxim}, i val ${numero}`
);
}
return numero;
}
export const entorn = {
// ...camps anteriors...
limitPaginaMaxim: numericaEnRang('LIMIT_PAGINA_MAXIM', 100, 1, 1000),
};Reutilitzem numerica(), que ja rebutja els valors no enters, i només hi afegim la comprovació de rang. Amb LIMIT_PAGINA_MAXIM=5000 l'arrencada avorta amb un missatge explícit, que és exactament el que volem: un límit de paginació mal configurat és un problema de disponibilitat (02-06), no un detall menor.
Solució 2
Què ha passat: en clonar només ha obtingut .env.example, perquè .env és al .gitignore i no viatja amb el repositori. Sense .env, dotenv no troba res per carregar, process.env.JWT_SECRET és undefined i la funció obligatoria() avorta l'arrencada.
Per què és desitjable: és un fail fast. L'alternativa seria arrencar amb jwtSecret === undefined, i llavors jsonwebtoken signaria (o fallaria) a la primera petició d'inici de sessió, en producció, amb un error críptic i a les tres de la matinada. Detectar el problema al segon zero, en arrencar, amb el nom exacte de la variable, és infinitament més barat.
Els dos passos: cp .env.example .env i omplir JWT_SECRET amb una cadena llarga i aleatòria, per exemple amb node -e "console.log(require('crypto').randomBytes(48).toString('hex'))".
Missatge millorat:
throw new Error(
`Falta la variable d'entorn obligatòria: ${nom}. ` +
`Copia .env.example a .env i omple-la (mira el README, secció "Posada en marxa").`
);Un bon missatge d'error no descriu el problema: descriu la solució.
Solució 3
| Paquet | Bloc | Justificació |
|---|---|---|
express |
dependencies |
El servidor no arrenca sense ell |
supertest |
devDependencies |
Només es fa servir a les proves de 03-08 |
dotenv |
dependencies |
S'executa en arrencar, també en producció |
prettier |
devDependencies |
Formata codi; irrellevant en temps d'execució |
better-sqlite3 |
dependencies |
És l'accés a dades de l'aplicació |
eslint |
devDependencies |
Anàlisi estàtica prèvia al desplegament |
jsonwebtoken |
dependencies |
Signa i verifica tokens a cada petició autenticada |
Si dotenv caigués a devDependencies: npm ci --omit=dev no l'instal·laria, i import 'dotenv/config' a src/config/entorn.js llançaria ERR_MODULE_NOT_FOUND en arrencar. El procés moriria abans d'escoltar al port. És una fallada sorollosa i immediata, cosa que és una sort; el cas veritablement perillós és el d'un paquet que només s'importa en una ruta poc freqüent, perquè llavors el desplegament sembla correcte i peta dies després. Val la pena assenyalar un matís: en producció real moltes vegades no hi ha .env en absolut —les variables les injecta l'orquestrador—, així que hi ha qui argumenta que dotenv és només de desenvolupament. Si la teva arrencada l'importa incondicionalment, és una dependència de producció i prou.
Conclusió
L'entorn està muntat i, més important, cada decisió està presa amb criteri i no per inèrcia: Node.js 20 LTS gestionat amb nvm per poder conviure amb altres projectes, ESM en lloc de CommonJS amb les conseqüències que això té a cada import, dependències amb rangs ^ avalades per un package-lock.json que sí que es versiona, npm ci reservat per a CI i producció, i una separació clara entre el que l'aplicació necessita per executar-se i el que només fem servir nosaltres en desenvolupar. Saps què aporta cadascun dels deu paquets instal·lats i a quina lliçó apareixerà.
Sobretot, has fixat dues coses que condicionen la resta del mòdul. La primera és l'estructura per capes —rutes, controladors, serveis, repositoris— que a 03-05 permetrà canviar el magatzem en memòria per SQLite sense tocar la lògica, i a 03-08 permetrà provar els serveis sense aixecar un servidor. La segona és la configuració a l'entorn: un .env que mai no es versiona, un .env.example que documenta, i un src/config/entorn.js que valida en arrencar i prefereix no arrencar abans que funcionar a mitges.
Tenim l'esquelet i ni una sola línia que respongui a una petició. A 03-02, Creació d'un servidor bàsic, això canvia: veurem què és realment Express i què és un middleware amb la seva signatura (req, res, next), separarem app.js de servidor.js —una decisió que sembla capriciosa fins que arriben les proves de 03-08—, muntarem el Router sota /v1 materialitzant el versionat a la ruta que vam decidir a 02-07, i retornarem els primers cafès reals, caf_001 i caf_002, amb l'embolcall {"dades": [...], "total": n} del contracte.
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
