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

  1. Què construirem i amb què
  2. Node.js 20 LTS: instal·lació i verificació
  3. Gestors de versions: nvm i fnm
  4. npm i npx
  5. Creació del projecte amb npm init
  6. Anatomia de package.json
  7. ESM davant de CommonJS i "type": "module"
  8. Versionat semàntic a les dependències: ^ i ~
  9. package-lock.json, npm ci davant de npm install
  10. dependencies davant de devDependencies
  11. Les dependències del curs, una per una
  12. Els scripts npm del projecte
  13. L'estructura de carpetes
  14. Configuració amb variables d'entorn
  15. .gitignore i inicialització de Git
  16. Editor i eines de treball
  17. Comprovació final de l'entorn

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

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

node --version
npm --version
v20.11.1
10.2.4

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, sense nodemon.
  • node:test i node --test: runner de proves integrat (03-08).
  • node --env-file: càrrega de fitxers .env sense llibreria (des de la 20.6).

  1. Gestors de versions: nvm i fnm

Podries 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·lades

Un 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:

echo "20" > .nvmrc

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.

  1. npm i npx

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

  1. Creació del projecte amb npm init

Creem la carpeta i la inicialitzem:

mkdir botiga-aroma-api
cd botiga-aroma-api
npm init -y
  • mkdir crea el directori del projecte. El nom en kebab-case és la convenció d'npm.
  • npm init -y genera un package.json amb 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"
}

  1. Anatomia de package.json

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

  1. ESM davant de CommonJS i "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

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 contenidora

Fixa'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.

  1. Versionat semàntic a les dependències: ^ 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.

  1. package-lock.json, npm ci davant de npm install

Si 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.json es 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 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.

  1. dependencies davant de devDependencies

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

  1. Les dependències del curs, una per una

Instal·lem primer les de producció:

npm install express@4 zod better-sqlite3 jsonwebtoken bcrypt dotenv
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:

npm install --save-dev supertest eslint prettier eslint-config-prettier
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@4 i 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 funcions async— que ens obliga a entendre de debò la gestió d'errors a 03-07. Direm al seu moment què canvia amb la 5.
  • bcrypt i no bcryptjs. 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 bcryptjs i 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.

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

  1. 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 req ni res, 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.

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

I .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=1h

Aquest 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 .env i n'aboca les claus a process.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 amb jwtSecret === undefined i 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", no 3000.
  • Object.freeze impedeix 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.

  1. .gitignore i inicialització de Git

El 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.json

Observa 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:

git init
git add .
git commit -m "Esquelet del projecte de l'API de la Botiga Aroma"

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.

  1. 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.recommended activa 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).
  • eqeqeq obliga a === en lloc de ==.
  • configPrettier va 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:

{
  "semi": true,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "all"
}

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

npm run dev

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:

curl -i http://localhost:3000/v1/cafes
  • -i inclou les capçaleres de resposta, imprescindible per comprovar Location, Link o Allow.
  • -v mostra a més la petició completa.
  • -s silencia la barra de progrés, útil en encadenar amb jq.

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.

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

Error: Falta la variable d'entorn obligatòria: JWT_SECRET

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é:

Error: Falta la variable d'entorn obligatòria: JWT_SECRET

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:

LIMIT_PAGINA_MAXIM=100

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

Mòdul 2: Disseny d'APIs RESTful

Mòdul 3: Desenvolupament d'APIs RESTful

Mòdul 4: Bones Pràctiques i Seguretat

Mòdul 5: Eines i Frameworks

Mòdul 6: Casos d'Estudi i Projectes

© Copyright 2026. Tots els drets reservats