Cerrábamos el módulo 2 con el contrato de Tienda Aroma completo sobre el papel: 24 URIs, métodos con su idempotencia resuelta, catálogo de errores, representaciones JSON, paginación, versionado y documentación. Ni una línea de servidor. Este módulo cumple ese contrato con Node.js 20 y Express, y como en cualquier obra seria, se empieza por los cimientos: preparar la máquina, crear el proyecto, elegir y entender cada dependencia, definir la estructura de carpetas que sostendrá ocho lecciones de código y aislar la configuración en variables de entorno. Es la lección menos vistosa del módulo y la que más problemas evita: casi todos los atascos de un principiante con Node vienen de una versión equivocada, un import que el proyecto no admite o un secreto escrito a fuego en el código. Al terminar tendrás el esqueleto del proyecto listo para que la siguiente lección lo arranque; todavía no habrá servidor, y eso es intencionado.
Contenido
- Qué vamos a construir y con qué
- Node.js 20 LTS: instalación y verificación
- Gestores de versiones:
nvmyfnm - npm y
npx - Creación del proyecto con
npm init - Anatomía de
package.json - ESM frente a CommonJS y
"type": "module" - Versionado semántico en las dependencias:
^y~ package-lock.json,npm cifrente anpm installdependenciesfrente adevDependencies- Las dependencias del curso, una por una
- Los scripts npm del proyecto
- La estructura de carpetas
- Configuración con variables de entorno
.gitignoree inicialización de Git- Editor y herramientas de trabajo
- Comprobación final del entorno
- Qué vamos a construir y con qué
Durante las ocho lecciones de este módulo vamos a construir un único proyecto que crece. No habrá ejemplos sueltos que se tiran a la basura: cada lección parte del estado en que la dejó la anterior y dice explícitamente qué ficheros crea y cuáles modifica.
| Lección | Qué añade al proyecto |
|---|---|
| 03-01 | Esqueleto: package.json, carpetas, configuración, Git |
| 03-02 | Servidor Express, router /v1, primeras rutas de cafés en memoria |
| 03-03 | Controladores, servicios, mapeadores, CRUD completo, filtros y paginación |
| 03-04 | Esquemas Zod y middleware de validación |
| 03-05 | SQLite con el patrón repositorio, migraciones, transacciones |
| 03-06 | Registro, login, JWT, roles y permisos |
| 03-07 | ErrorApi y middleware de errores unificado |
| 03-08 | Pruebas unitarias y de integración |
La pila técnica queda fijada aquí y no cambia:
| Pieza | Elección | Por qué |
|---|---|---|
| Ejecución | Node.js 20 LTS | Soporte a largo plazo, node --watch y node:test integrados |
| Módulos | ESM (import/export) |
Es el estándar de JavaScript; CommonJS es el legado |
| Framework HTTP | Express 4.x | Minimalista, explícito, el más extendido; nada de magia oculta |
| Validación | Zod | Esquemas declarativos con inferencia de tipos |
| Persistencia | SQLite vía better-sqlite3 |
Cero configuración, SQL real, sustituible por PostgreSQL |
| Autenticación | JWT (jsonwebtoken) + bcrypt |
Sin estado, encaja con REST |
| Pruebas | node:test + Supertest |
Sin dependencias extra para el runner |
Una nota sobre el idioma: como fijamos en la guía de estilo de 02-01, el código y los comentarios van en español. Verás obtenerCafes, precioEuros, repositorioCafes o manejadorErrores, y los ficheros del proyecto se llaman rutas/cafes.js o servicios/pedidos.js. Solo mantienen su nombre en inglés los que impone la herramienta: package.json, .env, node_modules.
- Node.js 20 LTS: instalación y verificación
Node.js es el entorno que ejecuta JavaScript fuera del navegador. Las versiones pares (18, 20, 22) son LTS (Long Term Support): reciben correcciones durante unos tres años y son las que se usan en producción. Las impares son experimentales.
Lo primero es ver qué hay instalado:
Si node --version responde v20.x.x, ya tienes lo necesario. Si responde v16.x.x o el comando no existe, sigue leyendo. Necesitamos 20 o superior por tres motivos concretos que usaremos en este módulo:
node --watch: recarga automática del servidor al guardar, sinnodemon.node:testynode --test: runner de pruebas integrado (03-08).node --env-file: carga de ficheros.envsin librería (desde 20.6).
- Gestores de versiones:
nvm y fnm
nvm y fnmPodrías instalar Node desde nodejs.org y terminar. No lo hagas. Un instalador deja una única versión global, y en cuanto trabajes en dos proyectos —uno en Node 18 y otro en Node 20— tendrás un problema que se resuelve desinstalando y reinstalando. Un gestor de versiones permite tener varias a la vez y cambiar de una a otra en segundos, incluso por carpeta.
Los dos habituales:
| Gestor | Escrito en | Ventaja | Plataformas |
|---|---|---|---|
| nvm | Bash | El más extendido, muchísima documentación | Linux, macOS (Windows: nvm-windows, proyecto distinto) |
| fnm | Rust | Mucho más rápido, cambio automático por carpeta | Linux, macOS, Windows nativo |
Instalación de nvm en Linux o macOS:
# Descarga e instala nvm (revisa la última versión en su repositorio)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Recarga la configuración del shell para que 'nvm' esté disponible
source ~/.bashrc # o ~/.zshrc si usas zshUso diario:
nvm install 20 # Instala la última 20.x LTS
nvm use 20 # Usa la 20 en esta terminal
nvm alias default 20 # La 20 será la versión por defecto al abrir una terminal nueva
nvm ls # Lista las versiones instaladasUn detalle muy práctico: si creas un fichero .nvmrc en la raíz del proyecto con el contenido 20, cualquiera que clone el repositorio puede ejecutar nvm use y obtener la versión correcta sin preguntar. Vamos a añadirlo:
Con fnm los comandos son casi idénticos (fnm install 20, fnm use 20) y además lee el .nvmrc automáticamente al entrar en la carpeta si lo configuras con --use-on-cd.
- npm y
npx
npxAl instalar Node vienen dos comandos que conviene no confundir:
| Comando | Qué hace | Ejemplo |
|---|---|---|
npm |
Gestor de paquetes: instala, actualiza y ejecuta scripts | npm install express |
npx |
Ejecuta un paquete sin instalarlo permanentemente | npx eslint src/ |
npx es especialmente útil para herramientas de un solo uso (generadores, migradores) y para ejecutar binarios que están en node_modules/.bin sin escribir la ruta completa.
Existen alternativas a npm —pnpm (más rápido y ahorra disco), yarn, bun—, y son perfectamente válidas. Este curso usa npm porque viene con Node y no añade un requisito más.
- Creación del proyecto con
npm init
npm initCreamos la carpeta y la inicializamos:
mkdircrea el directorio del proyecto. El nombre en kebab-case es la convención de npm.npm init -ygenera unpackage.jsoncon valores por defecto sin hacer preguntas. Sin-y, npm pregunta nombre, versión, descripción, etc. de forma interactiva.
El resultado es un package.json mínimo:
{
"name": "tienda-aroma-api",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
- Anatomía de
package.json
package.jsonpackage.json es la ficha de identidad del proyecto: quién es, qué necesita para funcionar y cómo se ejecuta. Vamos a sustituirlo por la versión definitiva del curso, campo a campo:
{
"name": "tienda-aroma-api",
"version": "1.0.0",
"description": "API RESTful de Tienda Aroma, tienda de café de especialidad",
"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 pruebas/",
"lint": "eslint src/ pruebas/",
"formato": "prettier --write \"**/*.{js,json,md}\""
},
"license": "UNLICENSED",
"private": true
}Qué significa cada campo:
| Campo | Para qué sirve |
|---|---|
name |
Identificador del paquete. En minúsculas, sin espacios |
version |
Versión del proyecto en formato SemVer (02-07). Ojo: no es la versión de la API, que va en la ruta /v1 |
description |
Texto libre; aparece en el registro de npm si se publica |
type |
"module" activa ESM. Es la decisión de la sección siguiente |
main |
Punto de entrada si otro paquete importa este |
engines |
Versiones de Node admitidas. npm avisa si no coinciden |
scripts |
Comandos abreviados que se lanzan con npm run <nombre> |
license |
UNLICENSED para código privado; MIT o similar para abierto |
private |
true impide publicarlo en npm por accidente. Imprescindible en código de empresa |
- ESM frente a CommonJS y
"type": "module"
"type": "module"Node arrastra dos sistemas de módulos y hay que elegir uno conscientemente, porque mezclarlos es la primera fuente de errores incomprensibles.
| Aspecto | CommonJS (el legado) | ESM (el estándar) |
|---|---|---|
| Importar | const express = require('express') |
import express from 'express' |
| Exportar | module.exports = algo |
export default algo / export { algo } |
| Cuándo se resuelve | En tiempo de ejecución | En tiempo de análisis (estático) |
| Activación | Por defecto | "type": "module" o extensión .mjs |
| Extensión en rutas propias | Opcional (./cafes) |
Obligatoria (./cafes.js) |
__dirname, __filename |
Disponibles | No existen (hay equivalentes) |
Top-level await |
No | Sí |
Tienda Aroma usa ESM. Es el estándar del lenguaje, funciona igual en el navegador y en el servidor, y permite await en el nivel superior de un fichero, algo que agradeceremos al abrir la base de datos en 03-05.
Las dos consecuencias prácticas que más despistan al principio:
// CORRECTO en ESM: la extensión .js es obligatoria en las rutas propias
import { repositorioCafes } from './repositorios/cafes-memoria.js';
// INCORRECTO en ESM: falta la extensión → ERR_MODULE_NOT_FOUND
import { repositorioCafes } from './repositorios/cafes-memoria';
// Los paquetes de node_modules NO llevan extensión
import express from 'express';// __dirname no existe en ESM. Equivalente:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const rutaFichero = fileURLToPath(import.meta.url); // Ruta absoluta de este fichero
const rutaCarpeta = dirname(rutaFichero); // Su carpeta contenedoraFíjate también en el prefijo node: (node:url, node:path, node:fs). Es la forma moderna y explícita de importar módulos internos de Node, y evita que un paquete malicioso llamado path en npm suplante al módulo del sistema. Úsalo siempre.
- Versionado semántico en las dependencias:
^ y ~
^ y ~Cuando instalas un paquete, npm escribe en package.json un rango de versiones aceptables, no una versión exacta. Recuerda SemVer de 02-07: MAYOR.MENOR.PARCHE.
| Rango | Significa | Acepta | No acepta |
|---|---|---|---|
^4.18.2 |
Compatible: fija la mayor | 4.18.3, 4.19.0 |
5.0.0 |
~4.18.2 |
Aproximado: fija mayor y menor | 4.18.3, 4.18.9 |
4.19.0 |
4.18.2 |
Exacta | solo 4.18.2 |
cualquier otra |
* o latest |
Cualquiera | todo | — |
El valor por defecto de npm es ^, y es un compromiso razonable: recibes correcciones de fallos y funcionalidades nuevas sin cambios rompedores, siempre que el autor respete SemVer. Nunca uses *: significa "instálame lo que sea", y un día tu build romperá sin que hayas tocado nada.
package-lock.json, npm ci frente a npm install
package-lock.json, npm ci frente a npm installSi package.json dice ^4.18.2, dos personas que instalen en fechas distintas pueden acabar con 4.18.2 y 4.19.1. Eso es exactamente lo que produce el clásico "en mi máquina funciona". La solución es package-lock.json: un fichero generado automáticamente que registra la versión exacta de cada paquete instalado y de cada dependencia de sus dependencias, con su hash de integridad.
Reglas de oro:
package-lock.jsonse versiona en Git. Siempre. No es un fichero temporal.- No se edita a mano jamás.
Y de ahí la diferencia entre los dos comandos de instalación:
npm install |
npm ci |
|
|---|---|---|
| Lee | package.json (y actualiza el lock) |
Solo package-lock.json |
| Puede cambiar versiones | Sí | No, nunca |
Borra node_modules antes |
No | Sí, entero |
| Velocidad | Menor | Mayor |
| Uso recomendado | Desarrollo, al añadir paquetes | Integración continua y producción |
Regla práctica: en tu portátil, npm install; en el pipeline de CI y en el servidor, npm ci (lo veremos en 05-05). Si npm ci falla porque el lock no concuerda con package.json, eso es una virtud, no un fallo: te está avisando de que alguien tocó las dependencias sin regenerar el lock.
dependencies frente a devDependencies
dependencies frente a devDependenciesnpm install express # va a "dependencies"
npm install --save-dev eslint # va a "devDependencies" (abreviado: -D)| Bloque | Contiene | ¿Se instala en producción? |
|---|---|---|
dependencies |
Lo que el código necesita para ejecutarse | Sí |
devDependencies |
Herramientas de desarrollo: pruebas, linters, formateadores | No (npm ci --omit=dev) |
La distinción no es cosmética: reduce el tamaño de la imagen de despliegue y, sobre todo, la superficie de ataque. Un linter no debería existir siquiera en el servidor de producción. El error inverso —poner en devDependencies algo que el servidor usa en tiempo de ejecución— produce un ERR_MODULE_NOT_FOUND que solo aparece al desplegar.
- Las dependencias del curso, una por una
Instalamos primero las de producción:
| Paquete | Qué hace | Dónde lo usaremos |
|---|---|---|
express |
Framework HTTP: enrutado, middleware, utilidades de petición y respuesta | 03-02 en adelante |
zod |
Validación por esquemas declarativos con inferencia de tipos | 03-04 |
better-sqlite3 |
Cliente SQLite síncrono, muy rápido, sin servidor de base de datos | 03-05 |
jsonwebtoken |
Firma y verificación de JWT | 03-06 |
bcrypt |
Hash de contraseñas con salt y coste configurable | 03-06 |
dotenv |
Carga variables de un fichero .env en process.env |
ahora mismo |
Y las de desarrollo:
| Paquete | Qué hace | Dónde lo usaremos |
|---|---|---|
supertest |
Lanza peticiones HTTP contra la app de Express sin abrir puerto | 03-08 |
eslint |
Detecta errores y malos usos antes de ejecutar | continuo |
prettier |
Formatea el código de forma consistente y automática | continuo |
eslint-config-prettier |
Desactiva las reglas de ESLint que chocan con Prettier | continuo |
Dos aclaraciones sobre elecciones que llaman la atención:
express@4y no 5. Express 5 ya es estable, pero la inmensa mayoría del código, la documentación y las respuestas que encontrarás son de la 4. Además, la 4 tiene una carencia muy didáctica —no captura los errores de funcionesasync— que nos obliga a entender de verdad el manejo de errores en 03-07. Diremos en su momento qué cambia con la 5.bcrypty nobcryptjs.bcryptes una extensión nativa (se compila al instalar) y es más rápida. Si su compilación falla en tu máquina —suele pasar en Windows sin herramientas de compilación—,bcryptjses un sustituto en JavaScript puro con la misma API:npm install bcryptjsy cambia elimport.
No necesitamos nodemon: node --watch hace lo mismo desde Node 18. Ni cors, helmet o express-rate-limit, que llegarán en el módulo 4 cuando toque endurecer la API.
- Los scripts npm del proyecto
Los scripts son la interfaz de la que dispone cualquiera que llegue al repositorio. Lo primero que hace un desarrollador nuevo es mirar scripts para saber cómo se arranca el proyecto.
"scripts": {
"dev": "node --watch src/servidor.js",
"start": "node src/servidor.js",
"test": "node --test pruebas/",
"lint": "eslint src/ pruebas/",
"formato": "prettier --write \"**/*.{js,json,md}\""
}| Script | Comando | Qué hace |
|---|---|---|
npm run dev |
node --watch |
Arranca y reinicia solo al guardar un fichero |
npm start |
node |
Arranca sin vigilancia. Es el de producción |
npm test |
node --test |
Ejecuta todas las pruebas de pruebas/ |
npm run lint |
eslint |
Analiza el código en busca de errores |
npm run formato |
prettier --write |
Reformatea todos los ficheros |
Detalle de npm que confunde: start y test son scripts "conocidos" y se invocan sin run (npm start, npm test); el resto necesita run (npm run dev). Ambos funcionan con run, así que en la duda, escribe npm run.
- La estructura de carpetas
Aquí está la decisión de arquitectura de todo el módulo. Vamos a organizar el código por capas, con una responsabilidad clara por carpeta:
mkdir -p src/{rutas,controladores,servicios,repositorios,esquemas,middleware,config,errores}
mkdir -p pruebas/{unitarias,integracion,ayudas}
mkdir -p migraciones| Carpeta / fichero | Responsabilidad | Lección |
|---|---|---|
src/servidor.js |
Arranca el proceso: lee el puerto y llama a listen() |
03-02 |
src/app.js |
Construye la aplicación Express y monta los middleware | 03-02 |
src/rutas/ |
Declara qué URI y método invoca a qué controlador | 03-02 |
src/controladores/ |
Traduce HTTP ↔ dominio: lee req, llama al servicio, escribe res |
03-03 |
src/servicios/ |
Lógica de negocio. No sabe que existe HTTP | 03-03 |
src/repositorios/ |
Acceso a datos. Lo único que sabe de la base de datos | 03-03 / 03-05 |
src/esquemas/ |
Esquemas Zod de validación de entrada | 03-04 |
src/middleware/ |
Piezas transversales: validación, autenticación, errores | 03-04 en adelante |
src/config/ |
Lectura y validación de la configuración del entorno | 03-01 |
src/errores/ |
ErrorApi y sus fábricas |
03-07 |
migraciones/ |
Ficheros .sql versionados que crean el esquema |
03-05 |
pruebas/ |
Pruebas unitarias, de integración y utilidades de apoyo | 03-08 |
¿Por qué tantas carpetas para una API pequeña? Porque cada frontera resuelve un problema real, y todas se cobran su beneficio dentro de este mismo módulo:
- Rutas separadas de controladores: el mapa de URIs de 02-02 se lee de un vistazo en un fichero, sin lógica en medio.
- Controladores separados de servicios: el servicio no toca
reqnires, así que se puede probar sin levantar un servidor (03-08) y reutilizar desde un script de línea de comandos o una tarea programada. - Servicios separados de repositorios: en 03-05 sustituimos el almacén en memoria por SQLite sin tocar una sola línea de los controladores ni de los servicios. Esa es la prueba de que la frontera vale la pena.
- Middleware aparte: validación, autenticación y errores son transversales; si viven dentro de las rutas, acaban duplicados en veinte sitios.
El flujo de una petición, de fuera hacia dentro, será siempre el mismo:
graph LR C[Cliente] --> R[rutas/] R --> M[middleware/] M --> CT[controladores/] CT --> S[servicios/] S --> RP[repositorios/] RP --> BD[(Datos)]
Y la regla que lo mantiene sano: las flechas nunca van hacia atrás. Un repositorio no llama a un servicio, y un servicio no importa nada de Express.
- Configuración con variables de entorno
El puerto, la ruta de la base de datos y el secreto de firma de los JWT no pueden estar escritos en el código. Cambian entre tu portátil, el entorno de pruebas y producción, y algunos son secretos.
El estándar de facto es la metodología Twelve-Factor App: la configuración vive en el entorno, no en el código. En Node se lee con process.env.
Creamos el fichero .env en la raíz:
# .env — configuración local. NO se sube a Git.
NODE_ENV=desarrollo
PUERTO=3000
BASE_URL=http://localhost:3000
RUTA_BASE_DATOS=./datos/aroma.db
JWT_SECRETO=cambia-esto-por-una-cadena-larga-y-aleatoria-en-produccion
JWT_CADUCIDAD=1hY .env.example, que sí se versiona, con las mismas claves pero sin valores reales:
# .env.example — plantilla. Cópiala a .env y rellena los valores.
NODE_ENV=desarrollo
PUERTO=3000
BASE_URL=http://localhost:3000
RUTA_BASE_DATOS=./datos/aroma.db
JWT_SECRETO=
JWT_CADUCIDAD=1hEste segundo fichero es documentación ejecutable: quien clone el repositorio hace cp .env.example .env, rellena y arranca. Sin él, la única forma de saber qué variables hacen falta es leer todo el código o esperar a que reviente.
14.1. src/config/entorno.js
Leer process.env.PUERTO disperso por todo el código es mala idea: no sabes qué variables existen, no hay valores por defecto centralizados y un error tipográfico produce undefined silencioso. Centralizamos la lectura en un único módulo que además valida al arrancar y falla ruidosamente si algo falta:
// src/config/entorno.js
import 'dotenv/config';
/**
* Lee una variable obligatoria. Si no existe, aborta el arranque.
* Fallar al arrancar es mucho mejor que fallar en la petición número 500.
*/
function obligatoria(nombre) {
const valor = process.env[nombre];
if (valor === undefined || valor.trim() === '') {
throw new Error(`Falta la variable de entorno obligatoria: ${nombre}`);
}
return valor;
}
/** Lee una variable opcional con valor por defecto. */
function opcional(nombre, porDefecto) {
const valor = process.env[nombre];
return valor === undefined || valor.trim() === '' ? porDefecto : valor;
}
/** Lee una variable numérica y comprueba que de verdad lo es. */
function numerica(nombre, porDefecto) {
const valor = opcional(nombre, String(porDefecto));
const numero = Number(valor);
if (!Number.isInteger(numero)) {
throw new Error(`La variable ${nombre} debe ser un número entero, y vale "${valor}"`);
}
return numero;
}
export const entorno = {
nodeEnv: opcional('NODE_ENV', 'desarrollo'),
puerto: numerica('PUERTO', 3000),
baseUrl: opcional('BASE_URL', 'http://localhost:3000'),
rutaBaseDatos: opcional('RUTA_BASE_DATOS', './datos/aroma.db'),
jwtSecreto: obligatoria('JWT_SECRETO'),
jwtCaducidad: opcional('JWT_CADUCIDAD', '1h'),
};
// Congelamos el objeto para que ninguna parte del código pueda modificarlo
// en caliente: la configuración se lee una vez y no cambia durante la ejecución.
Object.freeze(entorno);Línea a línea, lo importante:
import 'dotenv/config'ejecuta dotenv por su efecto secundario: lee.envy vuelca sus claves enprocess.env. Debe ocurrir antes de leer cualquier variable, y por eso está en la primera línea del primer módulo que se importa.obligatoria()lanza un error si la variable falta. Esto es deliberado: preferimos que el proceso no arranque a que arranque conjwtSecreto === undefinedy firme tokens inseguros durante semanas.numerica()convierte y comprueba. Recuerda que todas las variables de entorno son cadenas de texto:process.env.PUERTOes"3000", no3000.Object.freezeimpide reasignaciones accidentales.
A partir de ahora, cualquier fichero que necesite configuración hace import { entorno } from '../config/entorno.js' y usa entorno.puerto. Nadie más toca process.env.
En 03-04 conoceremos Zod y verás que este fichero podría escribirse con un esquema de cinco líneas. Lo dejamos en JavaScript puro a propósito: la configuración se valida antes de que exista nada más, y conviene que no dependa de terceros.
Como curiosidad útil: desde Node 20.6 existe node --env-file=.env src/servidor.js, que hace el trabajo de dotenv sin instalar nada. Mantenemos dotenv porque funciona igual en cualquier versión y en las herramientas de pruebas.
.gitignore e inicialización de Git
.gitignore e inicialización de GitEl fichero .env no se sube nunca a Git. Un secreto subido a un repositorio se considera comprometido para siempre, aunque borres el commit: queda en el historial, en los clones de tus compañeros y en las cachés de la plataforma. Rotarlo es la única solución, y es mucho más caro que escribir bien el .gitignore.
# .gitignore
# Dependencias
node_modules/
# Configuración local y secretos
.env
.env.*.local
# Base de datos local y sus ficheros auxiliares
datos/
*.db
*.db-journal
# Registros y cobertura
*.log
coverage/
# Sistema operativo y editores
.DS_Store
.vscode/*
!.vscode/extensions.jsonObserva que .env.example no está ignorado (solo lo está .env), que es justo lo que queremos. Y que package-lock.json tampoco: es un fichero que debe viajar con el proyecto.
Inicializamos el repositorio:
Antes de confirmar, verifica con git status que no aparece .env en la lista de ficheros añadidos. Si aparece, el .gitignore está mal o el fichero ya estaba indexado: git rm --cached .env lo saca del índice sin borrarlo del disco.
- Editor y herramientas de trabajo
16.1. Editor
Cualquier editor sirve, pero VS Code es el más común en el ecosistema Node y tiene integración directa con las herramientas del curso. Extensiones recomendadas: ESLint, Prettier - Code formatter y REST Client (permite lanzar peticiones desde un fichero .http, muy cómodo para probar la API sin salir del editor).
16.2. ESLint
ESLint analiza el código sin ejecutarlo y detecta variables sin usar, await olvidados o comparaciones sospechosas. Configuración 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 conjunto de reglas razonables que mantiene el propio ESLint.sourceType: 'module'le dice a ESLint que el código es ESM (coherente con"type": "module").argsIgnorePattern: '^_'permite argumentos sin usar si empiezan por guion bajo. Lo necesitaremos: el middleware de errores de Express obliga a declarar cuatro parámetros aunque no uses el último (03-07).eqeqeqobliga a===en lugar de==.configPrettierva el último y desactiva las reglas de estilo que se pisarían con Prettier.
Necesita un paquete más: npm install --save-dev @eslint/js.
16.3. Prettier
Prettier no opina sobre si el código es correcto, solo sobre cómo se ve, y elimina para siempre las discusiones sobre comillas y comas:
Guarda esto como .prettierrc.json. Con la extensión de VS Code y "Format on Save" activado, el formato deja de ser un tema de conversación.
16.4. Recarga automática
node --watch vigila los ficheros importados por el punto de entrada y reinicia el proceso al guardar. No confundir con node --watch-path, que vigila una carpeta concreta, ni con el hot reload del navegador: aquí el proceso se reinicia entero, así que el estado en memoria se pierde. En 03-02 y 03-03 los cafés viven en memoria, y notarás que un reinicio los devuelve a su valor inicial. Es normal y desaparece en 03-05 con SQLite.
16.5. curl y Postman
Durante todo el módulo probaremos con curl, que ya usamos en 01-03. Es universal, se copia y pega en cualquier documentación y no oculta nada:
-iincluye las cabeceras de respuesta, imprescindible para comprobarLocation,LinkoAllow.-vmuestra además la petición completa.-ssilencia la barra de progreso, útil al encadenar conjq.
Postman es un cliente gráfico con colecciones, entornos y pruebas automatizadas; es una herramienta excelente y le dedicamos entera la lección 05-01. Aquí no lo necesitamos.
- Comprobación final del entorno
El proyecto todavía no tiene servidor —eso es 03-02—, pero sí podemos verificar que los cimientos aguantan. Crea un fichero temporal comprobar.js en la raíz:
// comprobar.js — verificación del entorno. Se borra al terminar la lección.
import { entorno } from './src/config/entorno.js';
console.log('Node.js:', process.version);
console.log('Entorno:', entorno.nodeEnv);
console.log('Puerto configurado:', entorno.puerto, typeof entorno.puerto);
console.log('Base de datos:', entorno.rutaBaseDatos);
console.log('¿Hay secreto JWT?:', entorno.jwtSecreto ? 'sí' : 'no');Node.js: v20.11.1 Entorno: desarrollo Puerto configurado: 3000 number Base de datos: ./datos/aroma.db ¿Hay secreto JWT?: sí
Fíjate en number: la conversión de numerica() ha funcionado. Ahora prueba el camino de fallo, que es igual de importante: comenta la línea JWT_SECRETO= de tu .env y vuelve a ejecutar.
El proceso muere inmediatamente con un mensaje que dice exactamente qué falta. Ese es el comportamiento correcto. Restaura el .env y borra comprobar.js.
Estado del proyecto al terminar la lección:
tienda-aroma-api/
├── .env (ignorado por Git)
├── .env.example
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── eslint.config.js
├── package.json
├── package-lock.json
├── node_modules/ (ignorado por Git)
├── migraciones/ (vacía, se llena en 03-05)
├── pruebas/
│ ├── ayudas/
│ ├── integracion/
│ └── unitarias/
└── src/
├── config/
│ └── entorno.js
├── controladores/
├── errores/
├── esquemas/
├── middleware/
├── repositorios/
├── rutas/
└── servicios/Errores Comunes y Consejos
1. ERR_REQUIRE_ESM o Cannot use import statement outside a module. Falta "type": "module" en package.json, o estás usando require en un proyecto ESM. Decide un sistema y respétalo en todo el proyecto.
2. ERR_MODULE_NOT_FOUND con un fichero que sí existe. En ESM la extensión .js es obligatoria en las importaciones relativas. ./servicios/cafes falla; ./servicios/cafes.js funciona.
3. Subir .env a Git. El error más caro de esta lección. Escribe el .gitignore antes del primer git add. Si ya ha ocurrido, no basta con borrar el fichero: hay que rotar el secreto.
4. npm install en el servidor de producción. Puede instalar versiones distintas de las que probaste. Usa siempre npm ci, que respeta el lock al pie de la letra.
5. Añadir package-lock.json al .gitignore. Se ve más de lo que parece y anula toda la reproducibilidad. El lock se versiona.
6. Instalar globalmente (npm install -g) las dependencias del proyecto. Lo global no queda registrado en package.json, así que funciona en tu máquina y en ninguna otra. Solo se instalan globalmente herramientas de sistema, y a menudo ni eso: npx las ejecuta sin instalar.
7. Leer process.env desde media docena de ficheros. Céntralo en config/entorno.js. El día que una variable cambie de nombre, tocarás un sitio y no seis.
8. Confundir la versión de package.json con la versión de la API. "version": "1.0.0" es del artefacto de software; /v1 es del contrato. Pueden avanzar por separado: 1.4.7 sigue sirviendo /v1 (02-07).
Consejo: haz un commit al final de cada lección de este módulo. Si algo se rompe en 03-05, git diff te dirá en treinta segundos qué cambió respecto al estado bueno.
Ejercicios
Ejercicio 1
Añade a la configuración una variable nueva, LIMITE_PAGINA_MAXIMO, que fije el máximo de elementos por página decidido en 02-06. Debe ser numérica, opcional, con valor por defecto 100, y el arranque debe fallar si alguien escribe un valor no entero o mayor que 1000. Modifica .env, .env.example y src/config/entorno.js.
Ejercicio 2
Un compañero clona el repositorio y ejecuta npm start. Obtiene:
Explica qué ha pasado exactamente, por qué es el comportamiento deseable y qué dos pasos debe seguir. Después, propón una mejora del mensaje de error para que sea autoexplicativo.
Ejercicio 3
Clasifica estos paquetes en dependencies o devDependencies y justifica cada uno en una frase: express, supertest, dotenv, prettier, better-sqlite3, eslint, jsonwebtoken. Después indica qué ocurriría exactamente si dotenv acabara por error en devDependencies y se desplegara con npm ci --omit=dev.
Soluciones
Solución 1
En .env y .env.example:
En src/config/entorno.js, una función nueva y un campo más:
/** Lee una variable numérica y comprueba que está dentro de un rango. */
function numericaEnRango(nombre, porDefecto, minimo, maximo) {
const numero = numerica(nombre, porDefecto);
if (numero < minimo || numero > maximo) {
throw new Error(
`La variable ${nombre} debe estar entre ${minimo} y ${maximo}, y vale ${numero}`
);
}
return numero;
}
export const entorno = {
// ...campos anteriores...
limitePaginaMaximo: numericaEnRango('LIMITE_PAGINA_MAXIMO', 100, 1, 1000),
};Reutilizamos numerica(), que ya rechaza los valores no enteros, y solo añadimos la comprobación de rango. Con LIMITE_PAGINA_MAXIMO=5000 el arranque aborta con un mensaje explícito, que es exactamente lo que queremos: un límite de paginación mal configurado es un problema de disponibilidad (02-06), no un detalle menor.
Solución 2
Qué ha pasado: al clonar solo ha obtenido .env.example, porque .env está en .gitignore y no viaja con el repositorio. Sin .env, dotenv no encuentra nada que cargar, process.env.JWT_SECRETO es undefined y la función obligatoria() aborta el arranque.
Por qué es deseable: es un fail fast. La alternativa sería arrancar con jwtSecreto === undefined, y entonces jsonwebtoken firmaría (o fallaría) en la primera petición de login, en producción, con un error críptico y a las tres de la mañana. Detectar el problema en el segundo cero, en el arranque, con el nombre exacto de la variable, es infinitamente más barato.
Los dos pasos: cp .env.example .env y rellenar JWT_SECRETO con una cadena larga y aleatoria, por ejemplo con node -e "console.log(require('crypto').randomBytes(48).toString('hex'))".
Mensaje mejorado:
throw new Error(
`Falta la variable de entorno obligatoria: ${nombre}. ` +
`Copia .env.example a .env y rellénala (ver README, sección "Puesta en marcha").`
);Un buen mensaje de error no describe el problema: describe la solución.
Solución 3
| Paquete | Bloque | Justificación |
|---|---|---|
express |
dependencies |
El servidor no arranca sin él |
supertest |
devDependencies |
Solo se usa en las pruebas de 03-08 |
dotenv |
dependencies |
Se ejecuta en el arranque, también en producción |
prettier |
devDependencies |
Formatea código; irrelevante en tiempo de ejecución |
better-sqlite3 |
dependencies |
Es el acceso a datos de la aplicación |
eslint |
devDependencies |
Análisis estático previo al despliegue |
jsonwebtoken |
dependencies |
Firma y verifica tokens en cada petición autenticada |
Si dotenv cayera en devDependencies: npm ci --omit=dev no lo instalaría, y import 'dotenv/config' en src/config/entorno.js lanzaría ERR_MODULE_NOT_FOUND en el arranque. El proceso moriría antes de escuchar en el puerto. Es un fallo ruidoso e inmediato, lo cual es una suerte; el caso verdaderamente peligroso es el de un paquete que solo se importa en una ruta poco frecuente, porque entonces el despliegue parece correcto y revienta días después. Merece la pena señalar un matiz: en producción real muchas veces no hay .env en absoluto —las variables las inyecta el orquestador—, así que hay quien argumenta que dotenv es solo de desarrollo. Si tu arranque lo importa incondicionalmente, es una dependencia de producción y punto.
Conclusión
El entorno está montado y, más importante, cada decisión está tomada con criterio y no por inercia: Node.js 20 LTS gestionado con nvm para poder convivir con otros proyectos, ESM en lugar de CommonJS con las consecuencias que eso tiene en cada import, dependencias con rangos ^ respaldadas por un package-lock.json que sí se versiona, npm ci reservado para CI y producción, y una separación clara entre lo que la aplicación necesita para ejecutarse y lo que solo usamos nosotros al desarrollar. Sabes qué aporta cada uno de los diez paquetes instalados y en qué lección aparecerá.
Sobre todo, has fijado dos cosas que condicionan el resto del módulo. La primera es la estructura por capas —rutas, controladores, servicios, repositorios— que en 03-05 permitirá cambiar el almacén en memoria por SQLite sin tocar la lógica, y en 03-08 permitirá probar los servicios sin levantar un servidor. La segunda es la configuración en el entorno: un .env que nunca se versiona, un .env.example que documenta, y un src/config/entorno.js que valida al arrancar y prefiere no arrancar antes que funcionar a medias.
Tenemos el esqueleto y ni una sola línea que responda a una petición. En 03-02, Creación de un servidor básico, eso cambia: veremos qué es realmente Express y qué es un middleware con su firma (req, res, next), separaremos app.js de servidor.js —una decisión que parece caprichosa hasta que llegan las pruebas de 03-08—, montaremos el Router bajo /v1 materializando el versionado en la ruta que decidimos en 02-07, y devolveremos los primeros cafés reales, caf_001 y caf_002, con el envoltorio {"datos": [...], "total": n} del contrato.
Curso de REST API: Principios de Diseño y Desarrollo de APIs RESTful
Módulo 1: Introducción a las APIs RESTful
- ¿Qué es una API?
- Historia y evolución de las APIs
- Fundamentos de HTTP para APIs
- Principios básicos de REST
- Modelo de madurez de Richardson y HATEOAS
- REST vs. SOAP
- REST frente a GraphQL, gRPC y webhooks
Módulo 2: Diseño de APIs RESTful
- Principios de diseño de APIs RESTful
- Recursos y URIs
- Métodos HTTP
- Códigos de estado HTTP
- Representaciones, cabeceras y negociación de contenido
- Filtrado, ordenación, paginación y búsqueda
- Versionado de APIs
- Documentación de APIs
Módulo 3: Desarrollo de APIs RESTful
- Configuración del entorno de desarrollo
- Creación de un servidor básico
- Manejo de peticiones y respuestas
- Validación de datos de entrada
- Persistencia y capa de acceso a datos
- Autenticación y autorización
- Manejo de errores
- Pruebas y validación
Módulo 4: Buenas Prácticas y Seguridad
- Buenas prácticas en el diseño de APIs
- Seguridad en APIs RESTful
- OAuth 2.0 y OpenID Connect en la práctica
- Rate limiting y throttling
- CORS y políticas de seguridad
- Caché HTTP y rendimiento
- Observabilidad: logs, métricas y trazas
Módulo 5: Herramientas y Frameworks
- Postman para pruebas de APIs
- Swagger y OpenAPI para documentación
- Frameworks populares para APIs RESTful
- Contratos, mocks y pruebas automatizadas de API
- Integración continua y despliegue
- API gateways y portales de desarrollador
