Módulo 20: Node.js, npm y Vite
Objetivos del módulo
- Entender qué es Node.js, cómo funciona por dentro (V8, libuv y el event loop) y su modelo de versiones LTS
- Usar el REPL, el objeto
processy los módulos integrados con prefijonode:(fs,path,os,url,events,util) - Crear una API JSON sencilla con
node:http, y usar--env-file,--watchy el test runner integrado - Dominar npm:
package.json, dependencias, versionado semántico,package-lock.json, scripts ynpx - Protegerte de los ataques a la cadena de suministro y conocer pnpm, Yarn y Bun
- Entender qué es un bundler y crear, desarrollar, compilar y desplegar un proyecto con Vite
- Saber cuáles son los siguientes pasos: Express, Fastify y los frameworks de front-end
1. ¿Qué es Node.js?
📘 Concepto: Node.js es un entorno de ejecución de JavaScript fuera del navegador. Toma el motor V8 de Chrome (el que convierte tu JavaScript en código máquina) y le añade libuv, una librería en C que da acceso al sistema operativo: ficheros, red, procesos, temporizadores… Así, con el mismo lenguaje puedes escribir servidores web, herramientas de línea de comandos, scripts de automatización o aplicaciones de escritorio.
| Navegador | Node.js | |
|---|---|---|
| Motor | V8, SpiderMonkey, JavaScriptCore | V8 |
| Objeto global | window (y globalThis) | globalThis (sin window ni document) |
| APIs | DOM, localStorage, fetch, eventos de usuario | Ficheros, red, procesos, fetch, flujos (streams) |
| Módulos | ES modules | ES modules y CommonJS (require) |
| Seguridad | Aislado (sandbox) | Acceso completo al sistema |
El event loop en el servidor
Ya conoces el event loop del módulo 12. En Node funciona igual, pero en lugar de clics atiende peticiones de red y operaciones de disco: un único hilo de JavaScript reparte el trabajo y libuv realiza las operaciones lentas en segundo plano (con el sistema operativo o con un pequeño grupo de hilos).
flowchart LR
P1["Petición 1"] --> EL
P2["Petición 2"] --> EL
P3["Petición 3"] --> EL
EL["Event loop<br/>(un hilo de JavaScript)"] -->|"leer fichero, consultar BD..."| LIBUV["libuv<br/>(SO y grupo de hilos)"]
LIBUV -->|"callback / promesa resuelta"| EL
EL --> R["Respuestas"]
Es como un camarero muy rápido: toma la comanda de la mesa 1, la pasa a cocina y, sin esperar, atiende a la mesa 2. Cuando un plato está listo, lo lleva. Por eso Node maneja miles de conexiones simultáneas con poca memoria… siempre que no bloquees el hilo con cálculos pesados (para eso existen los worker threads, el equivalente a los Web Workers del módulo 18).
2. Versiones y LTS
Node.js publica versiones mayores con un calendario fijo. Hasta ahora salían dos al año: las pares (20, 22, 24, 26) se convertían en LTS (Long Term Support, soporte a largo plazo) y las impares eran de corta duración.
| Versión | Estado (octubre de 2026) | Fin de soporte |
|---|---|---|
| Node.js 20 | Fin de vida | Abril de 2026 |
| Node.js 22 | LTS de mantenimiento (solo seguridad) | Abril de 2027 |
| Node.js 24 | LTS activa (pasa a mantenimiento el 20 de octubre de 2026) | Abril de 2028 |
| Node.js 26 | Current; pasa a LTS el 28 de octubre de 2026 | Abril de 2029 |
⚠️ Importante: Desde Node.js 27 el modelo cambia: habrá una única versión mayor al año, publicada en abril y convertida en LTS en octubre, y todas serán LTS (desaparece la distinción entre pares e impares). Antes de cada versión habrá una fase alpha para probar cambios incompatibles; la de Node 27 empieza a finales de octubre de 2026.
💡 Consejo: En producción usa siempre una versión LTS. Para tener varias versiones instaladas y cambiar entre ellas, usa un gestor como fnm, nvm (macOS/Linux) o nvm-windows. Este curso usa Node.js 24 LTS.
node -v # v24.x.x
npm -v # 11.x.x
3. Ejecutar JavaScript con Node
El REPL
Si escribes node sin argumentos entras en el REPL (Read-Eval-Print Loop), una consola interactiva perfecta para probar cosas rápidas:
$ node
Welcome to Node.js v24.19.0.
Type ".help" for more information.
> 2 + 3
5
> const nombres = ['Ana', 'Luis']
undefined
> nombres.map((n) => n.toUpperCase())
[ 'ANA', 'LUIS' ]
> _ // el último resultado
[ 'ANA', 'LUIS' ]
> .exit
Comandos útiles: .help, .editor (modo multilínea), .exit (o pulsa Ctrl + C dos veces).
Ejecutar ficheros y módulos ES
node saludo.js
Node admite dos sistemas de módulos: los módulos ES (import/export, el estándar que estudiaste en el módulo 13) y el antiguo CommonJS (require/module.exports). Para que Node trate tus .js como módulos ES:
- Usa la extensión
.mjs, o bien - Añade
"type": "module"a tupackage.json(lo recomendado; lo verás en la sección 7).
💡 Consejo: En código nuevo usa siempre módulos ES. Los encontrarás con CommonJS en proyectos antiguos y en muchos tutoriales:
const fs = require('fs')equivale aimport fs from 'node:fs'.
El objeto process
process es un objeto global con información y control sobre el proceso en ejecución:
// info.js — ejecútalo con: node info.js Ana --verbose
console.log(process.argv);
// ['C:\\Program Files\\nodejs\\node.exe', 'C:\\proyecto\\info.js', 'Ana', '--verbose']
const [, , ...argumentos] = process.argv; // los dos primeros son node y el script
console.log(argumentos); // ['Ana', '--verbose']
console.log(process.cwd()); // carpeta desde la que se ha lanzado el comando
console.log(process.platform); // 'win32', 'linux', 'darwin'...
console.log(process.version); // 'v24.19.0'
console.log(process.env.HOME ?? process.env.USERPROFILE); // variables de entorno
if (argumentos.length === 0) {
console.error('Uso: node info.js <nombre>');
process.exit(1); // termina con código de error (0 = todo bien)
}
Para argumentos más complejos, util.parseArgs los analiza por ti:
import { parseArgs } from 'node:util';
// node saludar.js --nombre Ana -v
const { values } = parseArgs({
options: {
nombre: { type: 'string', default: 'mundo' },
verbose: { type: 'boolean', short: 'v' },
},
});
console.log(values); // { nombre: 'Ana', verbose: true }
⚠️ Importante: El código de salida importa: los scripts de npm, Git o la integración continua consideran que algo ha fallado si un proceso termina con un código distinto de 0.
4. Módulos integrados (node:)
Node trae una biblioteca estándar enorme. Impórtala con el prefijo node:, que deja claro que es un módulo integrado y no un paquete de npm con el mismo nombre.
| Módulo | Para qué sirve |
|---|---|
node:fs/promises | Leer, escribir y gestionar ficheros y carpetas (con promesas) |
node:path | Construir y analizar rutas de forma portable |
node:os | Información del sistema operativo |
node:url | URLs y conversión entre rutas y URLs file:// |
node:events | EventEmitter, el patrón de eventos de Node |
node:util | Utilidades: inspect, parseArgs, styleText, promisify… |
node:http | Servidores y clientes HTTP |
node:test / node:assert | Tests integrados |
node:crypto | Hashes, UUID (crypto.randomUUID()), cifrado |
node:child_process / node:worker_threads | Lanzar procesos / hilos |
Ficheros con node:fs/promises
// ficheros.js
import { readFile, writeFile, appendFile, mkdir, readdir, stat, rm } from 'node:fs/promises';
import path from 'node:path';
const carpeta = path.join(import.meta.dirname, 'datos');
await mkdir(carpeta, { recursive: true }); // no falla si ya existe
// Escribir y añadir texto
const notas = path.join(carpeta, 'notas.txt');
await writeFile(notas, 'Primera línea\n', 'utf8');
await appendFile(notas, 'Segunda línea\n');
// Leer texto (sin 'utf8' obtendrías un Buffer con los bytes)
const contenido = await readFile(notas, 'utf8');
console.log(contenido.split('\n').filter(Boolean)); // ['Primera línea', 'Segunda línea']
// JSON: escribir y leer
const fichero = path.join(carpeta, 'usuario.json');
await writeFile(fichero, JSON.stringify({ nombre: 'Ana', edad: 30 }, null, 2));
const usuario = JSON.parse(await readFile(fichero, 'utf8'));
console.log(usuario.nombre); // 'Ana'
// Listar la carpeta e información de un fichero
console.log(await readdir(carpeta)); // ['notas.txt', 'usuario.json']
console.log((await stat(notas)).size, 'bytes'); // 30 bytes
// Errores: el código indica la causa
try {
await readFile(path.join(carpeta, 'no-existe.txt'), 'utf8');
} catch (error) {
console.log(error.code); // 'ENOENT' (no existe el fichero)
}
await rm(carpeta, { recursive: true, force: true }); // borra la carpeta y su contenido
| Código de error | Significado |
|---|---|
ENOENT | No existe el fichero o la carpeta |
EACCES / EPERM | Sin permisos |
EEXIST | Ya existe |
EISDIR | Es una carpeta (esperabas un fichero) |
💡 Consejo: Para ficheros enormes (vídeos, logs de varios GB) no uses
readFile, que carga todo en memoria: usa streams (fs.createReadStream) ofor await (const linea of fichero.readLines()).
Rutas con node:path y node:url
Nunca construyas rutas concatenando con '/': Windows usa \.
import path from 'node:path';
console.log(path.join('datos', 'usuarios', '..', 'notas.txt')); // 'datos\notas.txt' en Windows
console.log(path.basename('/home/ana/informe.pdf')); // 'informe.pdf'
console.log(path.basename('/home/ana/informe.pdf', '.pdf')); // 'informe'
console.log(path.extname('foto.final.jpg')); // '.jpg'
console.log(path.dirname('/home/ana/informe.pdf')); // '/home/ana'
console.log(path.resolve('datos')); // ruta absoluta desde process.cwd()
console.log(path.parse('/home/ana/informe.pdf'));
// { root: '/', dir: '/home/ana', base: 'informe.pdf', ext: '.pdf', name: 'informe' }
En módulos ES no existen __dirname ni __filename (eran de CommonJS). Sus equivalentes:
console.log(import.meta.dirname); // carpeta del fichero actual
console.log(import.meta.filename); // ruta completa del fichero actual
console.log(import.meta.url); // 'file:///C:/proyecto/app.js'
import { fileURLToPath } from 'node:url';
console.log(fileURLToPath(import.meta.url) === import.meta.filename); // true
⚠️ Importante: Las rutas relativas en
readFile('datos.json')se resuelven desdeprocess.cwd()(donde lanzaste el comando), no desde la carpeta del script. Para leer un fichero que está junto a tu código, usapath.join(import.meta.dirname, 'datos.json').
node:os
import os from 'node:os';
console.log(os.platform(), os.arch()); // 'win32' 'x64'
console.log(os.availableParallelism()); // núcleos disponibles, p. ej. 8
console.log((os.totalmem() / 1024 ** 3).toFixed(1), 'GB'); // memoria total
console.log(os.homedir(), os.tmpdir()); // carpeta del usuario y temporal
console.log(JSON.stringify(os.EOL)); // '"\r\n"' en Windows, '"\n"' en Linux/macOS
node:events: EventEmitter
Muchos objetos de Node (servidores, streams, procesos) emiten eventos. Puedes crear los tuyos extendiendo EventEmitter:
import { EventEmitter, once } from 'node:events';
class Pedido extends EventEmitter {}
const pedido = new Pedido();
pedido.on('pagado', (id, total) => console.log(`Pedido ${id} pagado: ${total} €`));
pedido.once('enviado', (id) => console.log(`Pedido ${id} enviado (solo una vez)`));
pedido.emit('pagado', 42, 99.9); // 'Pedido 42 pagado: 99.9 €'
pedido.emit('enviado', 42); // 'Pedido 42 enviado (solo una vez)'
pedido.emit('enviado', 42); // (nada: once solo escucha la primera vez)
// Esperar un evento con await
setTimeout(() => pedido.emit('listo', 'ok'), 100);
const [valor] = await once(pedido, 'listo');
console.log(valor); // 'ok'
⚠️ Importante: Si un
EventEmitteremite'error'y nadie lo escucha, el proceso se cierra. Añade siempreemisor.on('error', ...)en los objetos que puedan fallar.
node:util
import util from 'node:util';
console.log(util.format('%s tiene %d años', 'Ana', 30)); // 'Ana tiene 30 años'
console.log(util.inspect({ a: { b: { c: { d: 1 } } } }, { depth: null })); // objeto completo
console.log(util.isDeepStrictEqual({ a: [1, 2] }, { a: [1, 2] })); // true
console.log(util.styleText('green', '✔ Todo correcto')); // texto en verde en la terminal
5. Un servidor HTTP con node:http
Vamos a crear una pequeña API REST de tareas que responde JSON, sin ninguna dependencia. Es la misma API que podrías consumir con fetch como en el módulo 17.
// servidor.js
import { createServer } from 'node:http';
const PUERTO = Number(process.env.PUERTO ?? 3000);
// "Base de datos" en memoria (se pierde al reiniciar)
const tareas = [
{ id: 1, titulo: 'Aprender Node.js', hecha: false },
{ id: 2, titulo: 'Publicar el proyecto', hecha: false },
];
let siguienteId = 3;
function enviarJSON(res, estado, datos) {
res.writeHead(estado, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(datos));
}
// El cuerpo de la petición llega en trozos (es un stream)
async function leerCuerpo(req) {
let cuerpo = '';
for await (const trozo of req) {
cuerpo += trozo;
if (cuerpo.length > 1e6) throw new Error('Cuerpo demasiado grande');
}
return cuerpo ? JSON.parse(cuerpo) : {};
}
const servidor = createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const partes = url.pathname.split('/').filter(Boolean); // ['api', 'tareas', '2']
console.log(`${req.method} ${url.pathname}`);
try {
if (partes[0] !== 'api' || partes[1] !== 'tareas') {
return enviarJSON(res, 404, { error: 'Ruta no encontrada' });
}
const id = partes[2] ? Number(partes[2]) : null;
// GET /api/tareas y GET /api/tareas?hecha=true
if (req.method === 'GET' && id === null) {
const filtro = url.searchParams.get('hecha');
const resultado = filtro === null ? tareas : tareas.filter((t) => String(t.hecha) === filtro);
return enviarJSON(res, 200, resultado);
}
// GET /api/tareas/:id
if (req.method === 'GET') {
const tarea = tareas.find((t) => t.id === id);
return tarea
? enviarJSON(res, 200, tarea)
: enviarJSON(res, 404, { error: `No existe la tarea ${id}` });
}
// POST /api/tareas
if (req.method === 'POST' && id === null) {
const { titulo } = await leerCuerpo(req);
if (typeof titulo !== 'string' || titulo.trim() === '') {
return enviarJSON(res, 400, { error: 'El título es obligatorio' });
}
const nueva = { id: siguienteId++, titulo: titulo.trim(), hecha: false };
tareas.push(nueva);
return enviarJSON(res, 201, nueva);
}
// DELETE /api/tareas/:id
if (req.method === 'DELETE' && id !== null) {
const indice = tareas.findIndex((t) => t.id === id);
if (indice === -1) return enviarJSON(res, 404, { error: `No existe la tarea ${id}` });
tareas.splice(indice, 1);
res.writeHead(204); // 204 No Content: éxito sin cuerpo
return res.end();
}
enviarJSON(res, 405, { error: 'Método no permitido' });
} catch (error) {
// JSON mal formado → 400; cualquier otra cosa → 500
enviarJSON(res, error instanceof SyntaxError ? 400 : 500, { error: error.message });
}
});
servidor.listen(PUERTO, () => {
console.log(`API escuchando en http://localhost:${PUERTO}/api/tareas`);
});
Pruébala con curl desde otra terminal (o con la extensión REST Client / Thunder Client de VS Code):
curl http://localhost:3000/api/tareas
# [{"id":1,"titulo":"Aprender Node.js","hecha":false},{"id":2,"titulo":"Publicar el proyecto","hecha":false}]
curl -X POST -H "Content-Type: application/json" -d '{"titulo":"Escribir tests"}' http://localhost:3000/api/tareas
# {"id":3,"titulo":"Escribir tests","hecha":false} (código 201)
curl http://localhost:3000/api/tareas/9
# {"error":"No existe la tarea 9"} (código 404)
curl -X DELETE http://localhost:3000/api/tareas/1 # código 204, sin cuerpo
⚠️ Importante: Si la llamas con
fetchdesde una página servida en otro origen (por ejemplo, Vite en el puerto 5173), el navegador la bloqueará por CORS (módulo 17). Para permitirlo, añade la cabecera'Access-Control-Allow-Origin': 'http://localhost:5173'y responde a las peticionesOPTIONS. Los frameworks de la sección 12 lo resuelven con una línea.
6. Herramientas integradas: --env-file, --watch y node:test
Variables de entorno con --env-file
La configuración que cambia entre entornos (puertos, URLs, claves de API) no debe estar escrita en el código. Se pone en variables de entorno, y en desarrollo se suelen guardar en un fichero .env:
# .env (¡no lo subas a Git!)
PUERTO=4000
API_KEY="clave secreta 123"
MODO=desarrollo
Node lo carga sin instalar nada (antes hacía falta el paquete dotenv):
node --env-file=.env servidor.js # falla si .env no existe
node --env-file-if-exists=.env servidor.js # lo carga solo si existe
console.log(process.env.PUERTO); // '4000' ← siempre texto
console.log(Number(process.env.PUERTO)); // 4000
// También desde el código: process.loadEnvFile('.env');
⚠️ Importante: Añade
.enva.gitignorey sube en su lugar un.env.examplecon las claves vacías, para que el resto del equipo sepa qué variables necesita.
Reinicio automático con --watch
node --watch servidor.js # reinicia el proceso cada vez que guardas un fichero importado
Sustituye a herramientas como nodemon para la mayoría de los casos.
El test runner integrado: node:test
Node incluye un sistema de tests completo, sin instalar nada:
// sumar.test.js
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { sumar } from './sumar.js';
describe('sumar', () => {
test('suma dos números', () => {
assert.equal(sumar(2, 3), 5);
});
test('lanza TypeError si recibe texto', () => {
assert.throws(() => sumar('2', 3), TypeError);
});
});
$ node --test
▶ sumar
✔ suma dos números (2.88ms)
✔ lanza TypeError si recibe texto (2.26ms)
✔ sumar (12.07ms)
ℹ tests 2
ℹ pass 2
ℹ fail 0
💡 Consejo:
node --testbusca automáticamente ficheros como*.test.jso los de la carpetatest/. Es ideal para librerías y backends; para proyectos de front-end con Vite usaremos Vitest (módulo 21), que tiene una API muy parecida.
7. npm: el gestor de paquetes
📘 Concepto: npm (Node Package Manager) se instala junto con Node y tiene dos partes: un registro público en
npmjs.comcon millones de paquetes (librerías reutilizables) y una herramienta de línea de comandos para instalarlos, actualizarlos y ejecutar tareas del proyecto.
Si un proyecto es una receta, los paquetes son los ingredientes ya preparados que compras en lugar de hacerlos desde cero, y package.json es la lista de la compra.
npm init y package.json
mkdir mi-proyecto
cd mi-proyecto
npm init -y # crea package.json con valores por defecto (sin preguntar)
npm pkg set type=module # usaremos módulos ES
⚠️ Importante: Las versiones actuales de npm (11.x) escriben
"type": "commonjs"de forma explícita al hacernpm init -y. Cámbialo a"module"(con el comando anterior o a mano) para usarimport/exporten los.js.
Un package.json típico, explicado:
{
"name": "gestor-tareas",
"version": "1.0.0",
"description": "API de tareas con Node.js",
"type": "module",
"main": "src/servidor.js",
"scripts": {
"start": "node --env-file-if-exists=.env src/servidor.js",
"dev": "node --watch --env-file-if-exists=.env src/servidor.js",
"test": "node --test"
},
"dependencies": {
"express": "^5.2.1"
},
"devDependencies": {
"eslint": "^10.12.0"
},
"engines": {
"node": ">=24"
},
"license": "MIT"
}
| Campo | Significado |
|---|---|
name / version | Identidad del paquete (obligatorios para publicarlo) |
type | "module" → los .js son módulos ES; "commonjs" → usan require |
main | Punto de entrada del paquete |
scripts | Comandos del proyecto que se ejecutan con npm run <nombre> |
dependencies | Paquetes necesarios para ejecutar la aplicación |
devDependencies | Paquetes necesarios solo para desarrollar (tests, linters, bundlers) |
engines | Versiones de Node compatibles |
private: true | Evita publicar el proyecto en npm por accidente |
Instalar, actualizar y desinstalar
npm install express # añade a dependencies (abreviado: npm i express)
npm install -D vitest # añade a devDependencies (--save-dev)
npm install express@5.2.1 # una versión exacta
npm install # instala TODO lo que dice package.json (al clonar un proyecto)
npm ci # instalación limpia y exacta desde package-lock.json (CI)
npm uninstall express # desinstala y lo quita de package.json
npm outdated # muestra qué paquetes tienen versiones nuevas
npm update # actualiza dentro de los rangos permitidos
npm ls --depth=0 # lista las dependencias directas
dependencies | devDependencies |
|---|---|
Express, Fastify, idb, temporal-polyfill, una librería de gráficos | Vite, Vitest, ESLint, Prettier, TypeScript |
| Se necesitan en producción | Solo en tu máquina y en la integración continua |
💡 Consejo: En un proyecto de front-end con Vite, todo acaba empaquetado en la carpeta
dist, así que la distinción es más conceptual. En un backend sí importa: en el servidor puedes instalar solo las de producción connpm ci --omit=dev.
Versionado semántico (SemVer)
Las versiones de los paquetes siguen el formato MAYOR.MENOR.PARCHE (por ejemplo, 5.2.1):
| Parte | Cuándo sube | Ejemplo |
|---|---|---|
| MAYOR | Cambios incompatibles (rompen tu código) | 4.21.0 → 5.0.0 |
| MENOR | Funcionalidades nuevas compatibles | 5.1.0 → 5.2.0 |
| PARCHE | Correcciones de errores compatibles | 5.2.0 → 5.2.1 |
En package.json, el símbolo delante de la versión indica qué actualizaciones aceptas:
| Rango | Acepta | Ejemplo con 5.2.1 |
|---|---|---|
^5.2.1 (por defecto) | Menores y parches | >=5.2.1 <6.0.0 |
~5.2.1 | Solo parches | >=5.2.1 <5.3.0 |
5.2.1 | Solo esa versión exacta | 5.2.1 |
>=5.0.0 | Cualquier versión superior | Peligroso |
* o latest | Cualquiera | Muy peligroso |
⚠️ Importante: En las versiones
0.x.ylas reglas son más estrictas:^0.5.1solo acepta>=0.5.1 <0.6.0, porque antes de la 1.0 cualquier versión menor puede romper la compatibilidad.
package-lock.json
package.json dice “acepto Express ^5.2.1”; package-lock.json dice exactamente qué versión de Express (y de cada una de sus cientos de subdependencias) se instaló, con su hash de integridad. Así, todo el equipo y el servidor de producción instalan el mismo árbol de dependencias.
- Súbelo siempre a Git. No lo edites a mano.
npm installpuede actualizarlo;npm cilo respeta al pie de la letra (y falla si no coincide conpackage.json), por eso se usa en integración continua.
node_modules y .gitignore
Los paquetes se descargan en la carpeta node_modules, que puede pesar cientos de MB y contiene código específico de tu sistema operativo. Nunca la subas a Git: se regenera con npm install.
# .gitignore
node_modules/
dist/
coverage/
.env
.env.local
*.log
npx: ejecutar paquetes sin instalarlos
npx cowsay "Hola" # descarga y ejecuta un paquete puntualmente
npx eslint . # ejecuta la versión instalada en el proyecto
npm create vite@latest # equivale a: npx create-vite@latest
💡 Consejo:
npxbusca primero ennode_modules/.bindel proyecto, así que ejecuta la versión del proyecto, no una global. Evita instalar herramientas de forma global (npm install -g): cada proyecto debe declarar las suyas endevDependencies.
Scripts de npm
Los scripts son atajos para los comandos del proyecto. Dentro de ellos puedes usar directamente los ejecutables de node_modules/.bin:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint .",
"format": "prettier --write .",
"check": "npm run lint && npm test -- --run"
}
}
npm run dev # ejecuta el script "dev"
npm test # "test" y "start" no necesitan "run"
npm run # lista todos los scripts disponibles
node --run dev # alternativa más rápida integrada en Node (sin pasar por npm)
8. Seguridad: la cadena de suministro
📘 Concepto: Cuando instalas un paquete, también instalas sus dependencias, las dependencias de estas… Un proyecto con Vite puede tener decenas de paquetes; uno con un framework, cientos. Todo ese código forma tu cadena de suministro (supply chain), y basta con que uno esté comprometido para que lo esté tu proyecto.
Los ataques reales más habituales:
| Ataque | Cómo funciona | Casos reales |
|---|---|---|
| Cuenta de mantenedor robada | Publican una versión maliciosa de un paquete popular | ua-parser-js (2021); chalk, debug y otros 16 paquetes muy usados (septiembre de 2025) |
| Gusano auto-replicante | El malware roba tokens de npm del desarrollador y se publica en sus otros paquetes | “Shai-Hulud” (septiembre y noviembre de 2025), cientos de paquetes |
| Typosquatting | Paquetes con nombres parecidos: expres, lodahs, crossenv | Constantes |
| Mantenedor “nuevo” malicioso | Alguien gana confianza y añade código malicioso | event-stream (2018) |
| Scripts de instalación | Un postinstall ejecuta código en tu máquina al hacer npm install | Habitual en los anteriores |
Buenas prácticas
npm audit # informa de vulnerabilidades conocidas en tus dependencias
npm audit fix # actualiza a versiones corregidas compatibles
npm audit signatures # verifica firmas y procedencia de los paquetes
- Piensa antes de instalar. ¿Necesitas un paquete para algo que el lenguaje ya hace (
structuredClone,Object.groupBy,Intl…)? Revisa en npmjs.com las descargas, el repositorio, la fecha de la última versión y el número de dependencias. - Comprueba el nombre exacto antes de instalar (cuidado con el typosquatting).
- Sube
package-lock.jsony usanpm cien integración continua y despliegues. - Espera unos días antes de adoptar versiones recién publicadas: la mayoría de paquetes maliciosos se detectan y retiran en horas o pocos días. Desde npm 11.10 puedes configurarlo en el
.npmrcdel proyecto conmin-release-age=7(en días); pnpm lo activa por defecto. - Desactiva los scripts de instalación cuando no los necesites:
npm install --ignore-scripts, oignore-scripts=trueen.npmrc. - Activa la autenticación en dos pasos (2FA) en tu cuenta de npm y de GitHub.
- Automatiza las actualizaciones con Dependabot o Renovate, revisando cada cambio.
- Nunca guardes secretos en el código ni en el repositorio.
⚠️ Importante:
npm auditsolo detecta vulnerabilidades ya conocidas. No te protege de un paquete malicioso publicado hace una hora: para eso sirven el retraso de adopción, el lockfile y desconfiar de los scripts de instalación.
9. Alternativas a npm: pnpm, Yarn y Bun
Todas leen el mismo package.json y usan el mismo registro de npm:
| Herramienta | Características principales |
|---|---|
| npm | Viene con Node. La opción por defecto y la que usa este curso |
| pnpm | Muy rápido y ahorra disco (guarda cada versión de un paquete una sola vez en el equipo). Estricto con las dependencias no declaradas. Seguro por defecto: no ejecuta scripts de instalación y aplica un retraso a las versiones nuevas |
| Yarn | Alternativa histórica de Meta; las versiones modernas (Berry) ofrecen Plug’n’Play sin node_modules |
| Bun | Además de gestor de paquetes, es un entorno de ejecución alternativo a Node (motor JavaScriptCore) con bundler y test runner integrados |
npm install → pnpm install → yarn → bun install
npm run dev → pnpm dev → yarn dev → bun dev
npx create-vite → pnpm dlx ... → yarn dlx → bunx ...
💡 Consejo: No mezcles gestores en el mismo proyecto: cada uno tiene su propio lockfile (
package-lock.json,pnpm-lock.yaml,yarn.lock,bun.lock). Puedes declarar cuál usa el proyecto con el campo"packageManager"depackage.json(por ejemplo,"pnpm@11.x.x").
10. Herramientas de build: qué es un bundler
En el módulo 13 cargaste módulos ES directamente en el navegador. Funciona, pero en un proyecto real aparecen problemas:
- Cientos de ficheros
import= cientos de peticiones HTTP. - Paquetes de npm importados por nombre (
import { openDB } from 'idb') que el navegador no sabe dónde buscar. - Código sin minificar, imágenes sin optimizar, CSS sin procesar…
📘 Concepto: Un bundler (empaquetador) recorre tus
importa partir de un punto de entrada, resuelve los paquetes denode_modulesy genera unos pocos ficheros optimizados: minificados (sin espacios ni nombres largos), con tree shaking (elimina el código que no usas) y con nombres con hash (index-CAoPt-vL.js) para que el navegador pueda guardarlos en caché indefinidamente.
flowchart LR
A["index.html"] --> B["main.js"]
B --> C["api.js"]
B --> D["ui.js"]
B --> E["idb (node_modules)"]
B --> F["style.css"]
C & D & E & F --> G{{"Bundler (Vite)"}}
G --> H["dist/index.html<br/>dist/assets/index-[hash].js<br/>dist/assets/index-[hash].css"]
Históricamente se usaba webpack; hoy la opción más popular para proyectos nuevos es Vite.
11. Vite
📘 Concepto: Vite (“rápido” en francés) es una herramienta de build con dos modos:
- Desarrollo: un servidor que sirve tus módulos ES casi sin transformarlos, de modo que arranca al instante, y con HMR (Hot Module Replacement): al guardar un fichero, el cambio aparece en el navegador sin recargar la página ni perder el estado.
- Producción: empaqueta y optimiza todo en la carpeta
dist(desde Vite 8, con Rolldown, un bundler escrito en Rust).
Crear un proyecto
npm create vite@latest mi-app -- --template vanilla
cd mi-app
npm install
npm run dev
El -- separa los argumentos de npm de los de create-vite. Si omites --template, un asistente te deja elegir entre vanilla (JavaScript sin framework), Vue, React, Preact, Lit, Svelte, Solid o Qwik, cada uno con su variante TypeScript (vanilla-ts, react-ts…).
VITE v8.3.2 ready in 312 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
Estructura del proyecto
mi-app/
├── index.html ← el punto de entrada (¡en la raíz, no en public!)
├── package.json
├── .gitignore
├── public/ ← ficheros que se copian tal cual (favicon, robots.txt)
│ └── favicon.svg
└── src/
├── main.js ← cargado desde index.html con <script type="module">
├── counter.js
├── style.css
└── assets/ ← imágenes que importas desde JS (se optimizan y llevan hash)
<!-- index.html -->
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
// src/main.js — Vite permite importar CSS, imágenes y JSON desde JavaScript
import './style.css';
import logo from './assets/javascript.svg'; // logo es la URL final del fichero
import { openDB } from 'idb'; // paquetes de npm por su nombre
document.querySelector('#app').innerHTML = `<img src="${logo}" alt="Logo de JavaScript">`;
{
"name": "mi-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^8.3.0"
}
}
Build y preview
npm run build # genera la carpeta dist/ optimizada
npm run preview # sirve dist/ en http://localhost:4173 para comprobar el resultado
vite v8.3.2 building client environment for production...
✓ 9 modules transformed.
dist/index.html 0.45 kB │ gzip: 0.29 kB
dist/assets/index-CsUDhMuy.css 4.10 kB │ gzip: 1.46 kB
dist/assets/index-CAoPt-vL.js 4.05 kB │ gzip: 1.77 kB
✓ built in 529ms
⚠️ Importante:
npm run devno sirve para producción: es un servidor de desarrollo. Lo que se publica es siempre el contenido dedist/.
Configuración: vite.config.js
No es obligatorio, pero lo necesitarás pronto:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
base: '/mi-app/', // ruta donde se publicará (p. ej., GitHub Pages de un repositorio)
server: { port: 3000, open: true },
build: { outDir: 'dist', sourcemap: true },
});
Variables de entorno VITE_
Vite carga automáticamente los ficheros .env, .env.local, .env.development y .env.production. Solo las variables que empiezan por VITE_ llegan a tu código:
# .env
VITE_API_URL=https://api.ejemplo.com
VITE_TITULO=Mi aplicación
CLAVE_PRIVADA=no-llega-al-navegador
console.log(import.meta.env.VITE_API_URL); // 'https://api.ejemplo.com'
console.log(import.meta.env.CLAVE_PRIVADA); // undefined (no empieza por VITE_)
console.log(import.meta.env.MODE); // 'development' o 'production'
console.log(import.meta.env.DEV, import.meta.env.PROD); // true false en desarrollo
console.log(import.meta.env.BASE_URL); // el valor de base
⚠️ Importante: Todo lo que empieza por
VITE_acaba escrito en el JavaScript que descarga el usuario: cualquiera puede verlo en DevTools. Nunca pongas ahí claves secretas. Los secretos se quedan en un backend.
Desplegar la carpeta dist
dist/ contiene solo ficheros estáticos (HTML, CSS, JS, imágenes), así que puedes publicarla en cualquier alojamiento estático:
| Servicio | Cómo |
|---|---|
| GitHub Pages | Con un workflow de GitHub Actions (lo harás paso a paso en el módulo 22). Recuerda configurar base: '/<repositorio>/' |
| Netlify / Vercel / Cloudflare Pages | Conectas el repositorio, comando npm run build, carpeta dist |
| Tu propio servidor | Copias dist/ a la carpeta pública de Nginx o Apache |
12. Siguientes pasos: frameworks de servidor y de front-end
Express y Fastify
Escribir un servidor con node:http es muy instructivo, pero en proyectos reales se usa un framework que resuelve el enrutado, el parseo de JSON, CORS, la validación y los errores:
// npm install express
import express from 'express';
const app = express();
app.use(express.json()); // parsea automáticamente los cuerpos JSON
const tareas = [{ id: 1, titulo: 'Aprender Express', hecha: false }];
app.get('/api/tareas', (req, res) => res.json(tareas));
app.get('/api/tareas/:id', (req, res) => {
const tarea = tareas.find((t) => t.id === Number(req.params.id));
if (!tarea) return res.status(404).json({ error: 'No encontrada' });
res.json(tarea);
});
app.post('/api/tareas', (req, res) => {
const nueva = { id: tareas.length + 1, titulo: req.body.titulo, hecha: false };
tareas.push(nueva);
res.status(201).json(nueva);
});
app.listen(3000, () => console.log('http://localhost:3000/api/tareas'));
| Framework | Características |
|---|---|
| Express (v5) | El más veterano y extendido; enorme ecosistema de middlewares |
| Fastify (v5) | Muy rápido, validación con JSON Schema integrada, sistema de plugins |
| Hono | Ligero y moderno; funciona en Node, Bun, Deno y en plataformas edge |
| NestJS | Estructurado y con TypeScript, inspirado en Angular; para equipos grandes |
Frameworks de front-end
Cuando una interfaz crece, gestionar a mano el DOM y el estado se complica. Los frameworks ofrecen componentes reactivos: describes cómo debe verse la interfaz según el estado, y ellos actualizan el DOM.
| Framework | Idea principal |
|---|---|
| React | Componentes con JSX; el ecosistema más grande (Next.js para aplicaciones completas) |
| Vue | Plantillas HTML con reactividad; curva de aprendizaje suave (Nuxt) |
| Svelte | Compila los componentes a JavaScript mínimo, sin runtime pesado (SvelteKit) |
| Angular | Framework completo de Google con TypeScript, muy usado en empresas |
Todos se crean con Vite (npm create vite@latest) o con su propia herramienta, y todo lo que has aprendido (módulos, promesas, fetch, DOM, eventos) es la base sobre la que funcionan.
13. Ejercicios
Ejercicio 1: Contador de palabras por línea de comandos
Crea contar.js que reciba la ruta de un fichero de texto (node contar.js libro.txt), y muestre el número de líneas, palabras y caracteres, y las 10 palabras más frecuentes. Si no se pasa ruta o el fichero no existe (ENOENT), muestra un mensaje en stderr y termina con process.exit(1).
Ejercicio 2: Organizador de descargas
Escribe un script que recorra una carpeta y mueva cada fichero a una subcarpeta según su extensión (imagenes/, documentos/, otros/), usando node:fs/promises y node:path. Añade una opción --simular (con util.parseArgs) que solo muestre lo que haría.
Ejercicio 3: API de tareas persistente
Amplía el servidor de la sección 5 para que guarde las tareas en un fichero tareas.json (léelo al arrancar y escríbelo tras cada cambio), añade PATCH /api/tareas/:id para marcar como hecha y lee el puerto de un .env con --env-file. Ejecútalo con --watch.
Ejercicio 4: Tests con node:test
Extrae la lógica de validación y filtrado de la API a un módulo tareas.js con funciones puras y escribe al menos seis tests con node:test y node:assert/strict. Añade el script "test": "node --test".
Ejercicio 5: Auditoría de dependencias
Crea un proyecto con npm init -y, instala tres paquetes de tu elección y responde: ¿cuántos paquetes hay en node_modules (npm ls --all)? ¿Qué rango de versión se ha guardado? ¿Qué dice npm audit? Añade un .npmrc con ignore-scripts=true y explica qué cambia.
Ejercicio 6: De módulos sueltos a Vite
Coge uno de los ejercicios del módulo 18 o 19 y conviértelo en un proyecto Vite (--template vanilla): mueve el código a src/, instala temporal-polyfill o idb con npm en lugar de usar un CDN, define la URL de una API en VITE_API_URL, y comprueba npm run build y npm run preview.
Ejercicio 7: La misma API con Express
Reescribe la API de tareas con Express 5 (o Fastify), con las mismas rutas y códigos de estado, añade CORS para http://localhost:5173 y consúmela desde el proyecto Vite del ejercicio anterior.
Resumen
| Concepto | Comando / sintaxis |
|---|---|
| Versión de Node | node -v (usa una LTS: 24, o 26 desde el 28-oct-2026) |
| REPL | node (salir con .exit) |
| Argumentos y salida | process.argv, util.parseArgs(), process.exit(1) |
| Módulos integrados | import { readFile } from 'node:fs/promises' |
| Carpeta del script | import.meta.dirname, path.join(import.meta.dirname, 'f.json') |
| Eventos | class X extends EventEmitter, .on(), .once(), .emit() |
| Servidor HTTP | createServer((req, res) => ...).listen(3000) |
| Variables de entorno | node --env-file=.env app.js, process.env.CLAVE |
| Recarga automática | node --watch app.js |
| Tests integrados | node --test con node:test y node:assert/strict |
| Crear proyecto | npm init -y + "type": "module" |
| Instalar | npm install paquete / npm install -D paquete / npm ci |
| Rangos SemVer | ^1.2.3 (menores y parches), ~1.2.3 (parches), 1.2.3 (exacta) |
| Scripts | npm run dev, npm test, node --run dev |
| Ejecutar sin instalar | npx paquete |
| Seguridad | npm audit, lockfile + npm ci, min-release-age, --ignore-scripts |
| Proyecto Vite | npm create vite@latest mi-app -- --template vanilla |
| Desarrollo / producción | npm run dev (HMR, puerto 5173) / npm run build → dist/ |
| Variables en Vite | .env con VITE_X → import.meta.env.VITE_X |