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 process y los módulos integrados con prefijo node: (fs, path, os, url, events, util)
  • Crear una API JSON sencilla con node:http, y usar --env-file, --watch y el test runner integrado
  • Dominar npm: package.json, dependencias, versionado semántico, package-lock.json, scripts y npx
  • 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 tu package.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 a import 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) o for 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 desde process.cwd() (donde lanzaste el comando), no desde la carpeta del script. Para leer un fichero que está junto a tu código, usa path.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 EventEmitter emite 'error' y nadie lo escucha, el proceso se cierra. Añade siempre emisor.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 fetch desde 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 peticiones OPTIONS. 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 .env a .gitignore y sube en su lugar un .env.example con 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 --test busca automáticamente ficheros como *.test.js o los de la carpeta test/. 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.com con 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 hacer npm init -y. Cámbialo a "module" (con el comando anterior o a mano) para usar import/export en 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 con npm 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.y las reglas son más estrictas: ^0.5.1 solo 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 install puede actualizarlo; npm ci lo respeta al pie de la letra (y falla si no coincide con package.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: npx busca primero en node_modules/.bin del 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 en devDependencies.

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
  1. 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.
  2. Comprueba el nombre exacto antes de instalar (cuidado con el typosquatting).
  3. Sube package-lock.json y usa npm ci en integración continua y despliegues.
  4. 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 .npmrc del proyecto con min-release-age=7 (en días); pnpm lo activa por defecto.
  5. Desactiva los scripts de instalación cuando no los necesites: npm install --ignore-scripts, o ignore-scripts=true en .npmrc.
  6. Activa la autenticación en dos pasos (2FA) en tu cuenta de npm y de GitHub.
  7. Automatiza las actualizaciones con Dependabot o Renovate, revisando cada cambio.
  8. Nunca guardes secretos en el código ni en el repositorio.

⚠️ Importante: npm audit solo 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" de package.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 import a partir de un punto de entrada, resuelve los paquetes de node_modules y 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 dev no sirve para producción: es un servidor de desarrollo. Lo que se publica es siempre el contenido de dist/.

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