Módulos, npm y pruebas

Lección 8 de 8. La última: cómo se reparte un programa de JavaScript en varios archivos (módulos), cómo se instalan paquetes con npm y cómo se comprueba con pruebas automáticas que el código hace lo que debe. Con un proyecto pequeño de verdad, montado y ejecutado archivo por archivo.

Por qué módulos

Hasta ahora, cada ejemplo cabía en un archivo. Un programa de verdad no: se reparte en archivos con una responsabilidad cada uno. En JavaScript, cada archivo es un módulo: lo que declara es privado salvo que lo exporte, y para usar algo de otro archivo hay que importarlo. Vamos a montar un proyecto pequeño, una tienda, con esta estructura:

tienda/
├── package.json
├── src/
│   ├── precios.js
│   ├── carrito.js
│   └── main.js
└── test/
    └── precios.test.js

package.json

Todo proyecto de Node empieza con un package.json, la ficha del proyecto: nombre, versión, dependencias y órdenes. npm init -y crea uno con los valores por omisión:

npm init -y
salidaWrote to /home/node/nuevo/package.json:

{
  "name": "nuevo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs"
}

Fíjate en la última línea: las versiones recientes de npm escriben "type": "commonjs", el sistema de módulos antiguo. Lo cambiamos, y dejamos el archivo así:

package.json

{
  "name": "tienda",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/main.js",
    "test": "node --test"
  }
}

"type": "module" le dice a Node que los archivos .js de este proyecto usan import y export. Sin esa línea, Node los trata como CommonJS, el sistema antiguo (más abajo). Y scripts define órdenes con nombre: npm start y npm test ejecutan lo que pone en cada una.

export e import

src/precios.js

export const IVA = 0.16;

export function conIva(precio) {
  return Math.round(precio * (1 + IVA) * 100) / 100;
}

export default function formatear(precio) {
  return `${precio.toFixed(2)} USD`;
}

Hay dos clases de exportación. Las con nombre (export const, export function) pueden ser muchas por archivo, y se importan con su nombre exacto entre llaves. La por defecto (export default) es una sola, y quien la importa le pone el nombre que quiera.

src/carrito.js

import { conIva } from "./precios.js";

export function total(lineas) {
  return lineas.reduce((suma, l) => suma + conIva(l.precio) * l.cantidad, 0);
}

src/main.js

import formatear, { conIva, IVA } from "./precios.js";
import * as carrito from "./carrito.js";

const lineas = [
  { nombre: "café", precio: 7.5, cantidad: 2 },
  { nombre: "miel", precio: 4.9, cantidad: 1 },
];

console.log(`IVA: ${IVA * 100} %`);
console.log("café con IVA:", formatear(conIva(7.5)));
console.log("total:", formatear(carrito.total(lineas)));
npm start
salida> tienda@1.0.0 start
> node src/main.js

IVA: 16 %
café con IVA: 8.70 USD
total: 23.08 USD

Diagrama de los módulos del proyecto: precios.js exporta a carrito.js, main.js y las pruebasCada archivo es un módulo: exporta algo y lo importa otrosrc/precios.jsexport const IVAexport function conIvaexport default formatearsrc/carrito.jsimport { conIva }export function totalsrc/main.jsimport formatear, { conIva, IVA }import * as carritotest/*.test.jsimport { conIva }date-fns(paquete de npm)conIvalas flechas van de quien exporta a quien importa; main.js no ve nada que no se exporte

Quién importa qué en el proyecto. precios.js no sabe quién lo usa: solo decide qué deja ver. Esquema: decodigo.com.
EscribesImporta
import { conIva } from "./precios.js"una exportación con nombre
import { conIva as iva } from "./precios.js"la misma, con otro nombre
import formatear from "./precios.js"la exportación por defecto, con el nombre que elijas
import * as precios from "./precios.js"todo, como propiedades de un objeto: precios.conIva
import fs from "node:fs"un módulo de Node (el prefijo node: lo deja claro)
import { format } from "date-fns"un paquete instalado con npm, sin ruta

Los tres errores de los módulos

En Node, la ruta de un archivo propio lleva la extensión completa. Sin ella no lo encuentra:

src/sin-extension.js

import { conIva } from "./precios";
console.log(conIva(10));
salidaError [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/node/tienda/src/precios' imported from /home/node/tienda/src/sin-extension.js

Un nombre que el otro archivo no exporta (aquí, por una mayúscula) falla antes de ejecutar nada, al enlazar los módulos:

src/mal-nombre.js

import { conIVA } from "./precios.js";
console.log(conIVA(10));
salidaSyntaxError: The requested module './precios.js' does not provide an export named 'conIVA'

Y require, que verás en muchísimos tutoriales, no existe en un módulo ES:

src/con-require.js

const fs = require("node:fs");
console.log(fs.existsSync("package.json"));
salidaReferenceError: require is not defined in ES module scope, you can use import instead

CommonJS: el sistema anterior

Antes de que import llegara al lenguaje en 2015, Node inventó su propio sistema, CommonJS, con require y module.exports. Sigue en millones de proyectos y paquetes, así que hay que saber leerlo. Un archivo .cjs es siempre CommonJS, diga lo que diga el package.json:

src/viejo-util.cjs

function conIva(precio) {
  return Math.round(precio * 1.16 * 100) / 100;
}
module.exports = { conIva };

src/viejo.cjs

const { conIva } = require("./viejo-util.cjs");
const path = require("node:path");

console.log(conIva(10), path.basename(__filename));
node src/viejo.cjs
salida11.6 viejo.cjs
Módulos ESCommonJS
Importarimport x from "./a.js"const x = require("./a")
Exportarexport, export defaultmodule.exports = ...
Archivos.mjs, o .js con "type": "module".cjs, o .js sin "type"
await fuera de funcionessíno
Funciona en el navegadorsí, con <script type="module">no

Para código nuevo, módulos ES: son el estándar del lenguaje y los mismos en Node y en el navegador. Cómo se cargan en una página está en el ejemplo la etiqueta script.

npm: instalar paquetes

npm es el gestor de paquetes que viene con Node, y su registro es el mayor del mundo. Para manejar fechas con algo más cómodo que Date, por ejemplo, la biblioteca date-fns:

npm install date-fns
salidaadded 1 package, and audited 2 packages in 2s

found 0 vulnerabilities

src/fechas.js

import { format, addDays, differenceInDays } from "date-fns";
import { es } from "date-fns/locale";

const pedido = new Date(2026, 9, 19);
const entrega = addDays(pedido, 3);

console.log(format(entrega, "EEEE d 'de' MMMM", { locale: es }));
console.log(differenceInDays(entrega, pedido), "días");
node src/fechas.js
salidajueves 22 de octubre
3 días

La instalación hace tres cosas: descarga el paquete en la carpeta node_modules, lo apunta en las dependencies del package.json con su versión (aquí, "date-fns": "^4.4.0") y fija la versión exacta de todo en package-lock.json.

Qué se guarda en git y qué no. package.json y package-lock.json, sí; node_modules, nunca: se regenera entera con npm install (o npm ci, que instala exactamente lo que dice el archivo de bloqueo). Y antes de instalar un paquete, mira cuántas descargas tiene y quién lo mantiene: cada dependencia es código de otra persona que se ejecuta con tus permisos.

Pruebas con node –test

Node trae su propio sistema de pruebas desde la versión 20, sin instalar nada. Un archivo de pruebas importa test y assert, y comprueba que el código hace lo que debe:

test/precios.test.js

import { test } from "node:test";
import assert from "node:assert/strict";
import { conIva } from "../src/precios.js";
import { total } from "../src/carrito.js";

test("conIva suma el 16 %", () => {
  assert.equal(conIva(100), 116);
});

test("conIva redondea a dos decimales", () => {
  assert.equal(conIva(4.9), 5.68);
});

test("el total de un carrito vacío es 0", () => {
  assert.equal(total([]), 0);
});

test("total multiplica por la cantidad", () => {
  assert.equal(total([{ precio: 10, cantidad: 3 }]), 34.8);
});
npm test
salida> tienda@1.0.0 test
> node --test

✔ conIva suma el 16 % (0.655061ms)
✔ conIva redondea a dos decimales (0.107782ms)
✔ el total de un carrito vacío es 0 (0.141636ms)
✔ total multiplica por la cantidad (0.1326ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 59.662574

npm test ejecuta node –test, que busca los archivos de prueba, ejecuta cada test y devuelve un resumenQué hace npm test en este proyectonpm testlee el script «test»node –testbusca *.test.jscada test()ejecuta sus assertresumenpass, fail y códigosi alguna prueba falla, el proceso termina con código 1:así es como un servidor de integración continua sabe que algo se rompió

npm test ejecuta el script del package.json; node --test encuentra los archivos que acaban en .test.js y ejecuta cada prueba. Esquema: decodigo.com.

node --test sin más argumentos busca solo los archivos de prueba: los que acaban en .test.js o están en una carpeta test, entre otros patrones. assert.equal compara con === (por eso se importa node:assert/strict) y assert.deepEqual compara objetos y arrays por su contenido, que con === no se puede, como se vio en la lección 5.

Cuando una prueba falla, la salida dice qué se esperaba y qué llegó. Para verlo sin estropear las pruebas buenas, la ponemos aparte, en fallos/redondeo.js: un nombre que no encaja en ningún patrón de prueba, porque con redondeo.test.js el npm test de arriba también la habría recogido (lo comprobamos: es lo que pasó en el primer intento).

fallos/redondeo.js

import { test } from "node:test";
import assert from "node:assert/strict";
import { conIva } from "../src/precios.js";

test("conIva de 7.5", () => {
  assert.equal(conIva(7.5), 8.69);
});
node --test fallos/redondeo.js
salida✖ conIva de 7.5 (1.053089ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 47.551718

✖ failing tests:

test at fallos/redondeo.js:5:1
✖ conIva de 7.5 (1.053089ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
  
  8.7 !== 8.69

7.5 por 1.16 da 8.7, no 8.69: la prueba estaba mal escrita, y es justo lo que una prueba que falla te obliga a mirar. El proceso termina con código de salida 1, que es lo que usan los servicios de integración continua para marcar el cambio como roto. Dos órdenes más que ahorran tiempo: node --test --watch repite las pruebas cada vez que guardas un archivo, y node --watch src/main.js hace lo mismo con el programa.

Y TypeScript

TypeScript es JavaScript con tipos: se declara qué es cada variable y cada parámetro, y un comprobador avisa de los errores antes de ejecutar. Se ha convertido en lo normal en proyectos grandes. Node 24 puede ejecutar un archivo .ts directamente:

src/factura.ts

interface Linea {
  nombre: string;
  precio: number;
  cantidad: number;
}

function subtotal(l: Linea): number {
  return l.precio * l.cantidad;
}

const lineas: Linea[] = [
  { nombre: "café", precio: 7.5, cantidad: 2 },
  { nombre: "miel", precio: 4.9, cantidad: 1 },
];
console.log(lineas.map(subtotal));
node src/factura.ts
salida[ 15, 4.9 ]

Pero ojo con lo que hace: borra los tipos y ejecuta lo que queda. No los comprueba. Este archivo tiene un error de tipos evidente y Node lo ejecuta igual:

src/tipo-mal.ts

const precio: number = "siete";
console.log(precio, typeof precio);
node src/tipo-mal.ts
salidasiete string

Para que alguien detecte ese error hace falta el compilador de TypeScript, tsc, o un editor que lo use por debajo. Y algunas construcciones de TypeScript que no son solo tipos, porque generan código, como los enum, no se pueden «borrar» sin más:

src/estado.ts

enum Estado {
  Pendiente,
  Enviado,
}
console.log(Estado.Enviado);
node src/estado.ts
salidaSyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

Lo que has aprendido en el curso

Con esta lección termina el curso. En ocho lecciones has pasado por los tipos y sus conversiones, el control de flujo, las funciones y los closures, los arrays y los objetos, las clases y this, los errores y la asincronía, y ahora los módulos, npm y las pruebas. Es todo lo que necesitas para leer casi cualquier código de JavaScript moderno.

Para comprobarlo, el test de JavaScript: 24 preguntas sobre el curso, que se corrigen en tu navegador. Para seguir practicando, los ejemplos de JavaScript, con tareas concretas de Node y del navegador. Y para reírte un rato (o llorar), las rarezas de JavaScript.

Todo el código de esta lección se ejecutó con Node.js 24 en un contenedor limpio antes de publicarla; las salidas y los errores están copiados de esa ejecución.