Llevamos cinco lecciones acumulando una deuda. En cada fichero de laboratorio has vuelto a copiar el array catalogo. Las clases Evento y Sesion que escribiste en el Módulo 1 siguen sin tener casa. Has escrito module.exports tres veces sin que nadie te explicara qué hace. Y en la lección Tu Primer Programa en Node.js dejamos una nota que decía, literalmente, "que esa duplicación te resulte incómoda es precisamente el motivo por el que existen los módulos".
Hoy pagamos esa deuda. Vas a entender el sistema de módulos CommonJS: cómo require encuentra los ficheros, qué hace exactamente module.exports, por qué reasignar exports no funciona, qué envoltorio invisible añade Node a cada fichero que ejecutas, y por qué un módulo se evalúa una sola vez en toda la vida del proceso, convirtiéndose de facto en un singleton.
Y al terminar, Escena Viva tendrá por fin una estructura de verdad: src/catalogo-datos.js exportando el catálogo, src/dominio/evento.js y src/dominio/sesion.js con las clases del Módulo 1, un src/dominio/index.js que las reexporta, y src/catalogo.js consumiéndolo todo sin una sola línea duplicada.
Contenido
- Por qué existen los módulos
- El sistema CommonJS:
requireymodule.exports exportsfrente amodule.exports- El envoltorio de módulo y sus cinco variables
- Tipos de módulo y algoritmo de resolución de
require - La caché de módulos: un módulo es un singleton
- Dependencias circulares
- Refactorización de Escena Viva
- Buenas prácticas de diseño de módulos
- Por qué existen los módulos
JavaScript nació sin módulos. Durante quince años, en el navegador, todos los scripts de una página compartían un único ámbito global, con las consecuencias previsibles:
<!-- El problema del ambito global compartido -->
<script src="catalogo.js"></script> <!-- define: var catalogo = [...] -->
<script src="informes.js"></script> <!-- tambien define: var catalogo = {} -->
<!-- El segundo pisa al primero. Nadie avisa. Todo se rompe. -->Tres problemas concretos:
| Problema | Consecuencia |
|---|---|
| Colisiones de nombres | Dos ficheros con la misma variable global se pisan silenciosamente |
| Dependencias implícitas | El orden de los <script> importa, pero no está escrito en ninguna parte |
| Nada es privado | Cualquier detalle interno es accesible y modificable desde cualquier sitio |
Node.js no podía permitirse eso: un servidor con cien ficheros y treinta dependencias externas habría sido inmanejable. Así que adoptó CommonJS, una especificación de módulos pensada para el lado del servidor, y la implantó desde su primera versión.
La idea central es de una simplicidad radical:
Cada fichero es un módulo. Todo lo que declaras dentro es privado, salvo lo que exportes explícitamente.
// src/utiles/formato.js
// PRIVADO: nadie fuera de este fichero puede ver esta constante.
const SIMBOLO_MONEDA = 'EUR';
// PRIVADO: funcion auxiliar interna.
function partirCentimos(centimos) {
return { euros: Math.floor(centimos / 100), resto: centimos % 100 };
}
// PUBLICO: solo esto sale del modulo.
function formatearPrecio(centimos) {
const { euros, resto } = partirCentimos(centimos);
return `${euros},${String(resto).padStart(2, '0')} ${SIMBOLO_MONEDA}`;
}
module.exports = { formatearPrecio };// Otro fichero
const { formatearPrecio } = require('./utiles/formato.js');
console.log(formatearPrecio(2500)); // 25,00 EUR
console.log(SIMBOLO_MONEDA); // ReferenceError: no existe aqui
console.log(partirCentimos); // ReferenceError: no existe aquiEsa privacidad por defecto es la propiedad más valiosa del sistema. Te permite cambiar los detalles internos de un módulo sin miedo, porque sabes con certeza que nadie fuera depende de ellos.
- El sistema CommonJS:
require y module.exports
require y module.exportsCommonJS tiene solo dos verbos.
module.exports: lo que el módulo ofrece
Cada módulo tiene un objeto module, y su propiedad exports es exactamente el valor que require devolverá. Puedes asignarle lo que quieras:
// Un objeto con varias cosas (lo mas habitual)
module.exports = { formatearPrecio, formatearFecha };
// Una sola funcion
module.exports = function calcularOcupacion(sesion) { /* ... */ };
// Una sola clase
module.exports = class Evento { /* ... */ };
// Un array de datos
module.exports = [ { id: 'evt-001' }, { id: 'evt-002' } ];
// Un valor primitivo (raro, pero legal)
module.exports = 3000;require: lo que el módulo necesita
require(ruta) carga un módulo y devuelve su module.exports:
// Importar el objeto completo
const formato = require('./utiles/formato.js');
console.log(formato.formatearPrecio(2500));
// Desestructurar solo lo que necesitas (preferido: se ve de un vistazo
// que usa este fichero)
const { formatearPrecio } = require('./utiles/formato.js');
// Renombrar al desestructurar, para evitar colisiones
const { formatearPrecio: precio } = require('./utiles/formato.js');Los dos estilos de exportación
| Estilo | Cuándo usarlo | Ejemplo de importación |
|---|---|---|
Objeto con nombres module.exports = { a, b } |
El módulo ofrece varias cosas relacionadas | const { a, b } = require('./m.js') |
Exportación única module.exports = X |
El módulo es una sola cosa: una clase, una función | const X = require('./m.js') |
En Escena Viva usaremos el objeto con nombres casi siempre, incluso para módulos con un único elemento. Razones:
- Es ampliable. Añadir una segunda exportación no rompe a nadie.
- El nombre viaja con el valor.
const { GestorDeVentas } = require(...)deja claro qué es, mientras queconst X = require(...)depende de que quien importa elija bien el nombre. - Es coherente con los módulos ES, que veremos en la lección siguiente.
exports frente a module.exports
exports frente a module.exportsAquí está la trampa clásica de CommonJS, y merece entenderse a fondo porque su explicación revela cómo funciona el sistema.
Node inyecta en cada módulo dos variables relacionadas: module y exports. Y al principio del fichero, se cumple esto:
// Lo que Node hace, conceptualmente, antes de ejecutar tu codigo:
const module = { exports: {} };
let exports = module.exports; // <-- Ambas apuntan al MISMO objetoflowchart LR
subgraph inicio["Al empezar el módulo"]
E1["exports"] --> O1["{ }<br/><i>el objeto de exportación</i>"]
M1["module.exports"] --> O1
end
Mientras añadas propiedades, las dos variables funcionan igual, porque manipulan el mismo objeto:
// src/utiles/formato.js
exports.formatearPrecio = formatearPrecio; // Funciona
exports.formatearFecha = formatearFecha; // Funciona
// Equivalente:
module.exports.formatearPrecio = formatearPrecio;Pero en el momento en que reasignas exports, la conexión se rompe:
flowchart LR
subgraph roto["Tras reasignar exports"]
E2["exports"] --> O3["{ formatearPrecio,<br/>formatearFecha }<br/><i>objeto nuevo, huérfano</i>"]
M2["module.exports"] --> O2["{ }<br/><i>el objeto que require devuelve</i>"]
end
style O3 stroke-dasharray: 5 5
exports pasa a apuntar a un objeto nuevo, pero module.exports sigue apuntando al original vacío. Y require devuelve module.exports, no exports.
Compruébalo:
// src/laboratorio/exports-roto.js
exports = { hola: () => 'hola' };
console.log('Dentro del modulo, exports:', exports); // { hola: [Function] }
console.log('Dentro del modulo, module.exports:', module.exports); // {}// src/laboratorio/probar-exports.js
const modulo = require('./exports-roto.js');
console.log(modulo); // {} <- vacio
console.log(modulo.hola); // undefinedLa regla, en una tabla:
| Escritura | ¿Funciona? | Por qué |
|---|---|---|
exports.nombre = valor |
Sí | Añade una propiedad al objeto compartido |
module.exports.nombre = valor |
Sí | Idéntico a lo anterior |
module.exports = { ... } |
Sí | Sustituye lo que require devolverá |
exports = { ... } |
No | Rompe la conexión; module.exports no cambia |
module.exports = X y luego exports.y = ... |
No para y |
Ya son objetos distintos |
Recomendación para el curso: usa siempre
module.exports = { ... }en una sola línea al final del fichero.Nunca tendrás este problema, y además el fichero gana algo valioso: un lugar único donde se ve toda su API pública. Un lector que abra el módulo puede ir al final y saber en dos segundos qué ofrece.
- El envoltorio de módulo y sus cinco variables
¿Cómo consigue Node que cada fichero tenga su propio ámbito, si JavaScript no tiene módulos? Con un truco de una elegancia notable: antes de ejecutar tu fichero, lo envuelve en una función.
Tu código:
Lo que Node ejecuta realmente:
(function (exports, require, module, __filename, __dirname) {
// ---- Tu codigo va aqui, intacto ----
const catalogo = [];
module.exports = { catalogo };
// ------------------------------------
});Esto se llama el envoltorio de módulo (module wrapper), y explica de golpe varias cosas:
- Por qué tus variables no contaminan el ámbito global: están dentro de una función.
- De dónde salen
require,moduleyexports: son parámetros de esa función, no variables globales. - Por qué
thisen el nivel superior de un módulo CommonJS esmodule.exports(un objeto vacío), y noglobal.
Puedes verlo con tus propios ojos:
Las cinco variables inyectadas:
| Variable | Qué es | Ejemplo de uso |
|---|---|---|
exports |
Atajo a module.exports (con la trampa del apartado 3) |
exports.formatear = fn |
require |
Función para cargar otros módulos | require('./formato.js') |
module |
Objeto que representa este módulo | module.exports = {...} |
__filename |
Ruta absoluta de este fichero | /home/joan/escena-viva/src/catalogo.js |
__dirname |
Ruta absoluta de la carpeta que lo contiene | /home/joan/escena-viva/src |
// src/laboratorio/variables-modulo.js
console.log('__filename:', __filename);
console.log('__dirname :', __dirname);
console.log('module.id :', module.id); // '.' si es el punto de entrada
console.log('this === module.exports:', this === module.exports); // true
console.log('Es el punto de entrada:', require.main === module);Dos usos prácticos que verás mucho:
__dirname para rutas fiables. El directorio de trabajo (process.cwd()) depende de desde dónde se ejecute el comando; __dirname no.
const path = require('node:path');
// MAL: depende de desde donde ejecutes node.
const rutaDatos = './datos/eventos.json';
// BIEN: siempre correcta, ejecutes desde donde ejecutes.
const rutaDatos = path.join(__dirname, '..', 'datos', 'eventos.json');Esto lo formalizaremos en la lección Rutas Multiplataforma con el Módulo path.
require.main === module para saber si eres el programa principal. Permite que un fichero sea a la vez módulo reutilizable y script ejecutable:
// src/informes/ocupacion.js
function generarInformeOcupacion(catalogo) {
// ... logica del informe ...
}
// Solo si se ejecuta directamente con "node src/informes/ocupacion.js"
if (require.main === module) {
const { catalogo } = require('../catalogo-datos.js');
console.log(generarInformeOcupacion(catalogo));
}
module.exports = { generarInformeOcupacion };Con esto, require('./informes/ocupacion.js') no imprime nada (solo exporta la función), pero node src/informes/ocupacion.js sí ejecuta el informe. Es un patrón muy útil y muy común.
- Tipos de módulo y algoritmo de resolución de
require
requireCuando escribes require('algo'), Node tiene que decidir qué fichero cargar. Sigue un algoritmo bien definido.
Los tres tipos de módulo
| Tipo | Cómo se escribe | Ejemplo | Dónde vive |
|---|---|---|---|
| Del núcleo | Nombre a secas, o con prefijo node: |
require('node:fs') |
Dentro del binario de Node |
| De fichero | Ruta que empieza por ./, ../ o / |
require('./dominio/evento.js') |
Tu proyecto |
| De paquete | Nombre a secas que no es del núcleo | require('express') |
node_modules/ |
Usa siempre el prefijo
node:para los módulos del núcleo:require('node:fs')en lugar derequire('fs'). Es la forma moderna y recomendada, elimina cualquier ambigüedad con un paquete de npm que se llamara igual, y es ligeramente más rápida porque Node se salta la búsqueda.
El algoritmo
flowchart TD
A["require('X')"] --> B{"¿X es un módulo<br/>del núcleo?"}
B -->|"Sí"| C["Devolver el módulo interno<br/>(fs, path, http, events...)"]
B -->|"No"| D{"¿X empieza por<br/>./ , ../ o / ?"}
D -->|"Sí"| E["Resolver como fichero:<br/>1. X tal cual<br/>2. X.js<br/>3. X.json<br/>4. X.node"]
E --> F{"¿Existe?"}
F -->|"Sí"| G["Cargar y devolver"]
F -->|"No"| H["Resolver como carpeta:<br/>1. X/package.json → campo main<br/>2. X/index.js<br/>3. X/index.json"]
H --> I{"¿Existe?"}
I -->|"Sí"| G
I -->|"No"| J["Error: MODULE_NOT_FOUND"]
D -->|"No"| K["Buscar en node_modules,<br/>subiendo por el árbol de carpetas"]
K --> L{"¿Encontrado?"}
L -->|"Sí"| G
L -->|"No"| J
Resolución de ficheros y carpetas
Node prueba extensiones y luego la interpretación como carpeta:
require('./dominio/evento')
// 1. ./dominio/evento (tal cual)
// 2. ./dominio/evento.js <-- normalmente aqui
// 3. ./dominio/evento.json
// 4. ./dominio/evento.node (complemento binario en C++)
require('./dominio')
// 1-4. Lo anterior con "dominio"...
// 5. ./dominio/package.json -> lee su campo "main"
// 6. ./dominio/index.js <-- el patron habitual
// 7. ./dominio/index.jsonEse punto 6 es la razón de que exista la convención del fichero index.js: permite que require('./dominio') cargue toda una carpeta. Lo usaremos en la refactorización.
Aunque las extensiones sean opcionales en CommonJS, escríbelas siempre:
require('./evento.js'), norequire('./evento'). Es más explícito, es un poco más rápido (Node no tiene que probar), y —sobre todo— es obligatorio en módulos ES, así que escribirlas te prepara para la lección siguiente y para migrar sin sorpresas.
La búsqueda ascendente en node_modules
Para un paquete como express, Node busca en node_modules subiendo carpeta a carpeta hasta la raíz del sistema:
Desde /home/joan/escena-viva/src/servidor/rutas.js, require('express') busca en:
/home/joan/escena-viva/src/servidor/node_modules/express
/home/joan/escena-viva/src/node_modules/express
/home/joan/escena-viva/node_modules/express <-- normalmente aqui
/home/joan/node_modules/express
/home/node_modules/express
/node_modules/expressPuedes ver esa lista en tiempo real:
Este mecanismo explica algo que confunde al principio: por qué a veces un paquete funciona sin estar en tu package.json. Estaba instalado en una carpeta superior y la búsqueda ascendente lo encontró. Es una dependencia accidental que dejará de funcionar en cuanto muevas el proyecto o alguien más lo instale desde cero. Lo veremos en el Módulo 5.
Cargar JSON directamente
CommonJS carga ficheros .json de forma nativa, ya analizados:
// Devuelve directamente el array, sin JSON.parse.
const catalogo = require('./datos/eventos.json');
console.log(catalogo.length); // 3Es cómodo y lo has usado en los node -p del Módulo 1. Pero tiene tres inconvenientes que hay que conocer:
- Es síncrono: bloquea el hilo principal mientras lee el fichero.
- Se cachea: si el fichero cambia en disco,
requiresigue devolviendo la versión antigua. - No existe en módulos ES sin sintaxis adicional.
Para configuración de arranque es perfectamente aceptable. Para datos que cambian —como el catálogo de Escena Viva— usaremos fs en el Módulo 3.
- La caché de módulos: un módulo es un singleton
Esta es la característica de CommonJS con más consecuencias prácticas:
Un módulo se evalúa UNA SOLA VEZ. La primera vez que se le hace
require, se ejecuta y sumodule.exportsse guarda en caché. Todas las llamadas posteriores devuelven exactamente el mismo objeto.
Demostrémoslo con un contador:
// src/laboratorio/contador.js
console.log('>> El modulo contador.js se esta EVALUANDO');
let cuenta = 0;
function incrementar() {
cuenta++;
return cuenta;
}
function valor() {
return cuenta;
}
module.exports = { incrementar, valor };// src/laboratorio/usar-contador.js
console.log('--- Primer require ---');
const primero = require('./contador.js');
console.log('--- Segundo require ---');
const segundo = require('./contador.js');
console.log('--- Tercer require ---');
const tercero = require('./contador.js');
console.log('');
console.log('¿Son el mismo objeto?', primero === segundo, segundo === tercero);
primero.incrementar();
primero.incrementar();
segundo.incrementar();
console.log('Valor visto desde "primero":', primero.valor());
console.log('Valor visto desde "tercero":', tercero.valor());--- Primer require --- >> El modulo contador.js se esta EVALUANDO --- Segundo require --- --- Tercer require --- ¿Son el mismo objeto? true true Valor visto desde "primero": 3 Valor visto desde "tercero": 3
Tres hechos en esa salida:
- El mensaje de evaluación aparece una sola vez, aunque hubo tres
require. - Los tres objetos son idénticos (
===). - El estado se comparte: incrementar desde una referencia lo ve todo el mundo.
En otras palabras: todo módulo CommonJS es un singleton dentro del proceso.
La utilidad
Es exactamente lo que quieres para recursos compartidos y caros de crear:
// src/datos/conexion.js
// El grupo de conexiones a la base de datos: uno solo en todo el proceso.
const grupo = crearGrupoDeConexiones({ maximo: 20 });
module.exports = { grupo };Da igual desde cuántos ficheros se le haga require: siempre es el mismo grupo de 20 conexiones, no veinte grupos. Lo mismo sirve para el GestorDeVentas de Escena Viva, para un registrador (logger) o para una caché en memoria. Este patrón es el que usaremos en el Módulo 7.
Los riesgos
Riesgo 1: estado compartido no deseado.
// PELIGRO: la configuracion es mutable y global.
// src/config.js
module.exports = { limiteEntradasPorPedido: 10 };
// En otro fichero, alguien hace:
const config = require('./config.js');
config.limiteEntradasPorPedido = 999; // Lo cambia para TODA la aplicacionRemedio: congelar lo que debe ser inmutable.
Riesgo 2: las pruebas se contaminan entre sí.
Si un módulo acumula estado y varias pruebas lo usan, la segunda prueba hereda el estado de la primera. Es una fuente clásica de pruebas que pasan en solitario y fallan en conjunto. Lo trataremos en el Módulo 9; para módulos con estado, la solución habitual es exportar una función fábrica en lugar de una instancia:
// En lugar de exportar una instancia (singleton forzoso)...
module.exports = { gestor: new GestorDeVentas(catalogo) };
// ...exporta la clase y deja que cada quien cree la suya.
module.exports = { GestorDeVentas };Riesgo 3: la clave de caché es la ruta resuelta. Dos rutas distintas que apunten al mismo fichero (por un enlace simbólico, o por mayúsculas y minúsculas en Windows) pueden producir dos instancias del mismo módulo. Es raro, pero cuando ocurre es desconcertante.
Inspeccionar y vaciar la caché
// Rutas de todos los modulos cargados
console.log(Object.keys(require.cache));
// Forzar que un modulo se vuelva a evaluar en el siguiente require
delete require.cache[require.resolve('./contador.js')];
const nuevo = require('./contador.js'); // Vuelve a evaluarse: cuenta = 0Manipular
require.cachees una herramienta de laboratorio, no de producción. Se usa en algunas configuraciones de pruebas y en recargadores en caliente de desarrollo. En código de aplicación es casi siempre señal de un problema de diseño.
- Dependencias circulares
Ocurre cuando A requiere a B y B requiere a A. Node no falla, pero el resultado sorprende.
// src/laboratorio/circular-a.js
console.log('A: empieza a evaluarse');
const b = require('./circular-b.js');
console.log('A: b vale', b);
module.exports = { nombre: 'modulo A' };
console.log('A: termina de evaluarse');// src/laboratorio/circular-b.js
console.log('B: empieza a evaluarse');
const a = require('./circular-a.js');
console.log('B: a vale', a); // <-- Aqui esta la sorpresa
module.exports = { nombre: 'modulo B' };
console.log('B: termina de evaluarse');A: empieza a evaluarse
B: empieza a evaluarse
B: a vale {} <-- Objeto VACIO, no { nombre: 'modulo A' }
B: termina de evaluarse
A: b vale { nombre: 'modulo B' }
A: termina de evaluarseLa explicación paso a paso:
| Paso | Qué ocurre |
|---|---|
| 1 | Se empieza a evaluar A. Node registra A en la caché con module.exports = {} (vacío) |
| 2 | A hace require('./circular-b.js'). Se empieza a evaluar B |
| 3 | B hace require('./circular-a.js'). A ya está en la caché, así que Node devuelve su module.exports actual: el objeto vacío |
| 4 | B termina y exporta lo suyo correctamente |
| 5 | A recibe el module.exports completo de B y termina |
La regla: en una dependencia circular, el módulo que se carga en segundo lugar recibe una versión incompleta del primero. No es un fallo de Node: es la única cosa razonable que puede hacer sin entrar en un bucle infinito.
Y por eso las dependencias circulares producen errores tan desconcertantes:
// En circular-b.js
const { crearEvento } = require('./circular-a.js');
crearEvento(); // TypeError: crearEvento is not a functionCómo evitarlas
| Técnica | Cómo |
|---|---|
| Extraer lo común a un tercer módulo | Si A y B comparten algo, ese algo va a C, y ambos dependen de C |
| Invertir la dependencia | En lugar de que A busque a B, que quien los usa a ambos les pase lo que necesitan |
Mover el require dentro de la función |
Se resuelve en tiempo de ejecución, cuando ambos módulos ya están completos. Es un parche, no una solución |
| Usar eventos | A emite y B escucha, sin que A conozca a B. Justo lo de la lección anterior |
Esa última fila no es casual. Muchas dependencias circulares son un síntoma de acoplamiento excesivo, y el patrón observador de la lección Eventos y EventEmitter es a menudo la solución de diseño correcta, no solo un truco para romper el ciclo.
Para detectarlas en un proyecto real existen herramientas como madge, que dibuja el grafo de dependencias y señala los ciclos.
- Refactorización de Escena Viva
Ha llegado el momento. Vamos a dejar el proyecto como debe estar al terminar el Módulo 2.
8.1 El punto de partida y el destino
ANTES DESPUES
escena-viva/ escena-viva/
├── datos/ ├── datos/
│ └── eventos.json │ └── eventos.json
└── src/ └── src/
├── catalogo.js <- datos ├── catalogo.js <- solo presentacion
│ incrustados ├── catalogo-datos.js <- solo datos
└── catalogo-datos.js <- sin ├── dominio/
module.exports │ ├── sesion.js
│ ├── evento.js
│ ├── gestor-ventas.js
│ └── index.js
└── utiles/
└── formato.jsflowchart TD
C["src/catalogo.js<br/><i>punto de entrada</i>"]
CD["src/catalogo-datos.js<br/><i>los datos</i>"]
DI["src/dominio/index.js<br/><i>fachada</i>"]
EV["src/dominio/evento.js"]
SE["src/dominio/sesion.js"]
GV["src/dominio/gestor-ventas.js"]
FO["src/utiles/formato.js"]
C --> CD
C --> DI
C --> FO
DI --> EV
DI --> SE
DI --> GV
EV --> SE
GV --> SE
Fíjate en la forma del grafo: todas las flechas van en una dirección. No hay ciclos, y las dependencias van de lo general (catalogo.js) a lo específico (sesion.js). Ese es el objetivo de todo diseño de módulos.
8.2 src/utiles/formato.js
Empezamos por abajo: el módulo que no depende de nada.
// src/utiles/formato.js
// Utilidades de presentacion de Escena Viva.
// Sin dependencias: es el modulo mas basico del proyecto.
// Convierte 2500 (centimos) en la cadena '25,00 EUR'.
function formatearPrecio(centimos) {
const euros = Math.floor(centimos / 100);
const resto = String(centimos % 100).padStart(2, '0');
return `${euros},${resto} EUR`;
}
// Convierte '2026-10-03T20:00:00' en '03/10/2026 20:00'.
function formatearFecha(fechaISO) {
const fecha = new Date(fechaISO);
const dia = String(fecha.getDate()).padStart(2, '0');
const mes = String(fecha.getMonth() + 1).padStart(2, '0');
const anio = fecha.getFullYear();
const hora = String(fecha.getHours()).padStart(2, '0');
const minuto = String(fecha.getMinutes()).padStart(2, '0');
return `${dia}/${mes}/${anio} ${hora}:${minuto}`;
}
// Genera un codigo de entrada: EV-2026-000123
function generarCodigoEntrada(anio, secuencia) {
return `EV-${anio}-${String(secuencia).padStart(6, '0')}`;
}
module.exports = { formatearPrecio, formatearFecha, generarCodigoEntrada };Esas tres funciones estaban duplicadas en varios ficheros del Módulo 1. Ahora existen una sola vez.
8.3 src/dominio/sesion.js
La clase Sesion que escribiste como solución de ejercicio en la lección JavaScript Moderno para Node.js, ahora con casa propia.
// src/dominio/sesion.js
// Una sesion es un pase concreto de un evento, con su fecha, aforo y precio.
const { formatearPrecio, formatearFecha } = require('../utiles/formato.js');
class Sesion {
// Campo privado: la unica forma de modificarlo es a traves de vender().
#vendidas = 0;
constructor({ id, fechaHora, aforo, vendidas = 0, precioCentimos }) {
this.id = id;
this.fechaHora = fechaHora;
this.aforo = aforo;
this.precioCentimos = precioCentimos;
this.#vendidas = vendidas;
}
get vendidas() {
return this.#vendidas;
}
get libres() {
return this.aforo - this.#vendidas;
}
get ocupacion() {
return Math.round((this.#vendidas / this.aforo) * 100);
}
get agotada() {
return this.libres === 0;
}
get precioEuros() {
return (this.precioCentimos / 100).toFixed(2);
}
// Recaudacion en centimos, entero.
get recaudacionCentimos() {
return this.#vendidas * this.precioCentimos;
}
vender(cantidad = 1) {
if (!Number.isInteger(cantidad) || cantidad < 1) {
const error = new Error('La cantidad debe ser un entero positivo');
error.codigo = 'CANTIDAD_INVALIDA';
throw error;
}
if (cantidad > this.libres) {
const error = new Error(`Aforo insuficiente en ${this.id}: quedan ${this.libres}`);
error.codigo = 'AFORO_INSUFICIENTE';
throw error;
}
this.#vendidas += cantidad;
return this.#vendidas;
}
// Linea de una sola fila para el listado por consola.
describir() {
return (
`${formatearFecha(this.fechaHora)} ${formatearPrecio(this.precioCentimos)} ` +
`${this.libres}/${this.aforo} libres (${this.ocupacion}% ocupado)` +
(this.agotada ? ' [AGOTADA]' : '')
);
}
// JSON.stringify llama automaticamente a toJSON si existe.
// Sin esto, el campo privado #vendidas no apareceria en la serializacion.
toJSON() {
return {
id: this.id,
fechaHora: this.fechaHora,
aforo: this.aforo,
vendidas: this.#vendidas,
precioCentimos: this.precioCentimos
};
}
}
module.exports = { Sesion };8.4 src/dominio/evento.js
// src/dominio/evento.js
// Un evento es un espectaculo programado, con una o varias sesiones.
const { Sesion } = require('./sesion.js');
class Evento {
#sesiones = [];
constructor({ id, titulo, sala, organizador, categoria, duracionMinutos, estado = 'publicado', sesiones = [] }) {
this.id = id;
this.titulo = titulo;
this.sala = sala;
this.organizador = organizador;
this.categoria = categoria;
this.duracionMinutos = duracionMinutos;
this.estado = estado;
// Convertimos los objetos planos en instancias de Sesion.
// Si ya lo son, los dejamos como estan.
this.#sesiones = sesiones.map((s) => (s instanceof Sesion ? s : new Sesion(s)));
}
get sesiones() {
// Copia defensiva: nadie de fuera puede anadir ni quitar sesiones.
return [...this.#sesiones];
}
get numeroSesiones() {
return this.#sesiones.length;
}
get aforoTotal() {
return this.#sesiones.reduce((total, s) => total + s.aforo, 0);
}
get entradasVendidas() {
return this.#sesiones.reduce((total, s) => total + s.vendidas, 0);
}
get entradasLibresTotales() {
return this.aforoTotal - this.entradasVendidas;
}
get ocupacion() {
if (this.aforoTotal === 0) return 0;
return Math.round((this.entradasVendidas / this.aforoTotal) * 100);
}
get recaudacionCentimos() {
return this.#sesiones.reduce((total, s) => total + s.recaudacionCentimos, 0);
}
get agotado() {
return this.#sesiones.every((s) => s.agotada);
}
buscarSesion(idSesion) {
return this.#sesiones.find((s) => s.id === idSesion);
}
entradasLibres(idSesion) {
const sesion = this.buscarSesion(idSesion);
return sesion ? sesion.libres : 0;
}
reservar(idSesion, cantidad = 1) {
const sesion = this.buscarSesion(idSesion);
if (!sesion) {
const error = new Error(`La sesion ${idSesion} no existe en ${this.id}`);
error.codigo = 'SESION_NO_ENCONTRADA';
throw error;
}
// Delegamos en Sesion: es ella quien sabe validar su propio aforo.
return sesion.vender(cantidad);
}
toString() {
return `${this.titulo} (${this.sala}) - ${this.ocupacion}% ocupado`;
}
toJSON() {
return {
id: this.id,
titulo: this.titulo,
sala: this.sala,
organizador: this.organizador,
categoria: this.categoria,
duracionMinutos: this.duracionMinutos,
estado: this.estado,
sesiones: this.#sesiones.map((s) => s.toJSON())
};
}
static desdeJSON(objeto) {
return new Evento(objeto);
}
}
module.exports = { Evento };Fíjate en la mejora de diseño respecto a la versión del Módulo 1: Evento.reservar ya no valida el aforo a mano, sino que delega en sesion.vender(). Cada clase sabe validar lo suyo. Esto solo es posible ahora que Sesion existe como módulo independiente y Evento puede requerirla.
8.5 src/dominio/index.js: la fachada
// src/dominio/index.js
// Fachada del dominio: un unico punto de entrada para todo el modelo.
// Permite escribir require('./dominio') en lugar de tres require distintos.
const { Sesion } = require('./sesion.js');
const { Evento } = require('./evento.js');
const { GestorDeVentas, UMBRAL_AFORO_BAJO } = require('./gestor-ventas.js');
module.exports = { Sesion, Evento, GestorDeVentas, UMBRAL_AFORO_BAJO };Este patrón —un index.js que reexporta— se llama fachada o barrel, y aporta dos cosas:
// Sin fachada: tres lineas, y quien importa debe conocer la estructura interna.
const { Sesion } = require('./dominio/sesion.js');
const { Evento } = require('./dominio/evento.js');
const { GestorDeVentas } = require('./dominio/gestor-ventas.js');
// Con fachada: una linea, y la estructura interna puede cambiar sin romper nada.
const { Sesion, Evento, GestorDeVentas } = require('./dominio');La segunda ventaja es la importante: si mañana divides evento.js en dos ficheros, solo cambias el index.js. Nadie más se entera.
8.6 src/catalogo-datos.js: por fin, un módulo
Aquí se salda la deuda concreta del Módulo 1.
// src/catalogo-datos.js
// Fuente de datos del catalogo de Escena Viva.
// Hasta el modulo 3 los datos estan aqui incrustados; despues se leeran
// de datos/eventos.json con fs, y este fichero sera el unico que cambie.
const catalogo = [
{
id: 'evt-001',
titulo: 'Concierto de Otono',
sala: 'Teatro Almendra',
organizador: 'org-almendra',
categoria: 'concierto',
duracionMinutos: 95,
estado: 'publicado',
sesiones: [
{ id: 'ses-001-1', fechaHora: '2026-10-03T20:00:00', aforo: 420, vendidas: 180, precioCentimos: 2500 },
{ id: 'ses-001-2', fechaHora: '2026-10-04T19:00:00', aforo: 420, vendidas: 96, precioCentimos: 2200 }
]
},
{
id: 'evt-002',
titulo: 'Noche de Monologos',
sala: 'Sala Boveda',
organizador: 'org-boveda',
categoria: 'humor',
duracionMinutos: 80,
estado: 'publicado',
sesiones: [
{ id: 'ses-002-1', fechaHora: '2026-10-10T21:30:00', aforo: 120, vendidas: 118, precioCentimos: 1800 },
{ id: 'ses-002-2', fechaHora: '2026-10-11T21:30:00', aforo: 120, vendidas: 45, precioCentimos: 1800 },
{ id: 'ses-002-3', fechaHora: '2026-10-17T21:30:00', aforo: 120, vendidas: 12, precioCentimos: 1500 }
]
},
{
id: 'evt-003',
titulo: 'Festival de Jazz de Primavera',
sala: 'Auditorio Ribera',
organizador: 'org-ribera',
categoria: 'festival',
duracionMinutos: 240,
estado: 'publicado',
sesiones: [
{ id: 'ses-003-1', fechaHora: '2027-04-17T19:00:00', aforo: 900, vendidas: 640, precioCentimos: 3800 },
{ id: 'ses-003-2', fechaHora: '2027-04-18T19:00:00', aforo: 900, vendidas: 720, precioCentimos: 4200 }
]
}
];
// Devuelve una copia profunda para que nadie modifique la fuente por accidente.
// structuredClone es nativo en Node desde la version 17.
function obtenerCatalogo() {
return structuredClone(catalogo);
}
function obtenerEventoPorId(id) {
const evento = catalogo.find((e) => e.id === id);
return evento ? structuredClone(evento) : undefined;
}
module.exports = { obtenerCatalogo, obtenerEventoPorId };Dos decisiones importantes:
- Exportamos funciones, no el array directamente. Si exportáramos
module.exports = { catalogo }, cualquiera podría modificar los datos de origen y, por la caché de módulos, esa modificación afectaría a todo el proceso. Devolver una copia constructuredCloneprotege la fuente. - La firma será la misma cuando lleguen los datos reales. En el Módulo 3,
obtenerCatalogo()leerádatos/eventos.jsonconfsy pasará a ser asíncrona. Ningún consumidor tendrá que cambiar su estructura, solo añadir unawait. Esa es la razón de existir de esta capa.
8.7 src/catalogo.js: el punto de entrada
// src/catalogo.js
// Punto de entrada del catalogo por consola de Escena Viva.
// Uso:
// node src/catalogo.js
// node src/catalogo.js --sala="Teatro Almendra"
// node src/catalogo.js --tabla
// node src/catalogo.js --max=2000
const { obtenerCatalogo } = require('./catalogo-datos.js');
const { Evento } = require('./dominio');
const { formatearPrecio } = require('./utiles/formato.js');
// --- Lectura de argumentos ---
function leerOpciones(argumentos) {
const opciones = { sala: null, tabla: false, precioMaximoCentimos: Infinity };
for (const argumento of argumentos) {
if (argumento === '--tabla') {
opciones.tabla = true;
} else if (argumento.startsWith('--sala=')) {
opciones.sala = argumento.slice('--sala='.length);
} else if (argumento.startsWith('--max=')) {
opciones.precioMaximoCentimos = Number(argumento.slice('--max='.length));
}
}
return opciones;
}
// --- Presentacion ---
function mostrarDetalle(eventos) {
console.log('');
console.log('==========================================');
console.log(' ESCENA VIVA - CATALOGO DE EVENTOS');
console.log('==========================================');
for (const evento of eventos) {
console.log('');
console.log(`${evento.titulo} [${evento.id}]`);
console.log(` Sala : ${evento.sala}`);
console.log(` Categoria : ${evento.categoria}`);
console.log(` Duracion : ${evento.duracionMinutos} min`);
console.log(` Sesiones : ${evento.numeroSesiones}`);
console.log(` Libres : ${evento.entradasLibresTotales} entradas`);
console.log(` Ocupacion : ${evento.ocupacion}%`);
for (const sesion of evento.sesiones) {
// La sesion sabe describirse a si misma: catalogo.js no calcula nada.
console.log(` - ${sesion.describir()}`);
}
}
console.log('');
}
function mostrarTabla(eventos) {
const filas = eventos.flatMap((evento) =>
evento.sesiones.map((sesion) => ({
evento: evento.id,
titulo: evento.titulo,
sala: evento.sala,
sesion: sesion.id,
precio: formatearPrecio(sesion.precioCentimos),
libres: sesion.libres,
ocupacion: `${sesion.ocupacion}%`
}))
);
console.table(filas);
}
// --- Programa principal ---
function principal() {
const opciones = leerOpciones(process.argv.slice(2));
// Los datos planos se convierten en objetos de dominio.
let eventos = obtenerCatalogo().map((datos) => Evento.desdeJSON(datos));
if (opciones.sala) {
eventos = eventos.filter((evento) => evento.sala === opciones.sala);
}
if (opciones.precioMaximoCentimos < Infinity) {
eventos = eventos.filter((evento) =>
evento.sesiones.some((s) => s.precioCentimos <= opciones.precioMaximoCentimos)
);
}
if (eventos.length === 0) {
// Diagnostico por stderr, segun la convencion del proyecto.
console.error('No hay eventos que cumplan los criterios indicados.');
process.exitCode = 1;
return;
}
if (opciones.tabla) {
mostrarTabla(eventos);
} else {
mostrarDetalle(eventos);
}
const aforoTotal = eventos.reduce((t, e) => t + e.aforoTotal, 0);
const vendidas = eventos.reduce((t, e) => t + e.entradasVendidas, 0);
const recaudacion = eventos.reduce((t, e) => t + e.recaudacionCentimos, 0);
console.error(
`${eventos.length} eventos | aforo ${aforoTotal} | vendidas ${vendidas} | ` +
`libres ${aforoTotal - vendidas} | recaudacion ${formatearPrecio(recaudacion)}`
);
}
// Solo se ejecuta si este fichero es el programa principal.
if (require.main === module) {
principal();
}
module.exports = { leerOpciones, principal };Comprobación:
┌─────────┬───────────┬────────────────────────────────┬────────────────────┬─────────────┬─────────────┬────────┬───────────┐ │ (index) │ evento │ titulo │ sala │ sesion │ precio │ libres │ ocupacion │ ├─────────┼───────────┼────────────────────────────────┼────────────────────┼─────────────┼─────────────┼────────┼───────────┤ │ 0 │ 'evt-001' │ 'Concierto de Otono' │ 'Teatro Almendra' │ 'ses-001-1' │ '25,00 EUR' │ 240 │ '43%' │ │ 1 │ 'evt-001' │ 'Concierto de Otono' │ 'Teatro Almendra' │ 'ses-001-2' │ '22,00 EUR' │ 324 │ '23%' │ │ 2 │ 'evt-002' │ 'Noche de Monologos' │ 'Sala Boveda' │ 'ses-002-1' │ '18,00 EUR' │ 2 │ '98%' │ │ 3 │ 'evt-002' │ 'Noche de Monologos' │ 'Sala Boveda' │ 'ses-002-2' │ '18,00 EUR' │ 75 │ '38%' │ │ 4 │ 'evt-002' │ 'Noche de Monologos' │ 'Sala Boveda' │ 'ses-002-3' │ '15,00 EUR' │ 108 │ '10%' │ │ 5 │ 'evt-003' │ 'Festival de Jazz de Primavera'│ 'Auditorio Ribera' │ 'ses-003-1' │ '38,00 EUR' │ 260 │ '71%' │ │ 6 │ 'evt-003' │ 'Festival de Jazz de Primavera'│ 'Auditorio Ribera' │ 'ses-003-2' │ '42,00 EUR' │ 180 │ '80%' │ └─────────┴───────────┴────────────────────────────────┴────────────────────┴─────────────┴─────────────┴────────┴───────────┘ 3 eventos | aforo 3000 | vendidas 1811 | libres 1189 | recaudacion 62298,00 EUR
Los totales coinciden con la semilla del Módulo 1: 3000 de aforo, 1811 vendidas, 1189 libres. La refactorización no ha cambiado ni un dato.
Y lo importante: mira lo que ya no hay en catalogo.js. No hay array de datos, no hay formatearPrecio duplicado, no hay cálculos de ocupación. Solo hay lectura de argumentos y presentación. Cada módulo hace una cosa.
- Buenas prácticas de diseño de módulos
Las reglas que seguiremos en todo el curso.
9.1 Una responsabilidad por módulo
Si al describir un módulo tienes que usar "y", probablemente son dos:
| Mal | Bien |
|---|---|
utiles.js con formateo, validación, fechas y cálculos |
formato.js, validacion.js, fechas.js |
evento.js que además lee el fichero de datos |
evento.js (modelo) + catalogo-datos.js (acceso) |
Un utiles.js que crece sin límite es el destino de todo lo que nadie sabe dónde poner. Cuando pase, divídelo.
9.2 Exporta poco
La API pública de un módulo es un compromiso. Todo lo que exportas es algo que alguien puede usar y que, por tanto, no podrás cambiar sin romper código ajeno.
// MAL: expone detalles internos que nadie de fuera necesita.
module.exports = {
formatearPrecio,
partirCentimos, // Auxiliar interno
SIMBOLO_MONEDA, // Constante interna
cacheDeFormatos // Estado interno
};
// BIEN: solo lo que forma parte del contrato.
module.exports = { formatearPrecio };Empieza exportando el mínimo. Ampliar una API es fácil; reducirla, no.
9.3 Sin efectos secundarios en la carga
Un require debería ser barato y seguro: definir cosas, no hacerlas.
// MAL: al requerir este modulo se abre una conexion, se lee un fichero
// y se imprime por consola. Sin que nadie lo haya pedido.
const conexion = conectarABaseDeDatos();
const datos = fs.readFileSync('datos/eventos.json');
console.log('Modulo de catalogo cargado');
module.exports = { conexion, datos };// BIEN: el modulo define capacidades. Quien las quiera, las invoca.
function conectar(opciones) { /* ... */ }
function cargarDatos(ruta) { /* ... */ }
module.exports = { conectar, cargarDatos };Un módulo con efectos secundarios es imposible de probar en aislamiento, hace que el arranque sea lento e impredecible, y —por la caché— sus efectos ocurren una sola vez en un momento que no controlas.
La excepción legítima es el patrón require.main === module del apartado 4: efectos solo cuando el fichero es el programa.
9.4 Sitúa los require al principio
// Al principio del fichero, agrupados y ordenados:
const path = require('node:path'); // 1. Nucleo de Node
const express = require('express'); // 2. Paquetes externos
const { Evento } = require('./dominio'); // 3. Modulos propiosAsí, cualquiera que abra el fichero ve sus dependencias en tres segundos. Un require escondido en mitad de una función es una dependencia que nadie va a encontrar.
9.5 Evita los require condicionales
// MAL: la dependencia solo se descubre en tiempo de ejecucion.
if (process.env.MODO === 'produccion') {
registrador = require('./registrador-produccion.js');
}Carga ambos y elige, o usa una fábrica. La única excepción razonable es la carga perezosa de un módulo muy pesado que rara vez se usa, y aun así conviene documentarlo.
9.6 Prefiere fábricas a instancias cuando haya estado
// Menos flexible: fuerza un singleton y complica las pruebas.
module.exports = { gestor: new GestorDeVentas(catalogo) };
// Mas flexible: cada quien crea el suyo cuando y como quiera.
module.exports = { GestorDeVentas };Exporta la instancia solo cuando el singleton es deliberado y deseado: un grupo de conexiones, un registrador global, una caché compartida.
Errores Comunes y Consejos
Error 1: exports = { ... } en lugar de module.exports = { ... }.
El módulo exporta un objeto vacío y quien lo importa recibe undefined en todo. Usa siempre module.exports.
Error 2: olvidar la extensión en rutas relativas.
Funciona en CommonJS, pero es ambiguo y no funcionará en módulos ES. Escribe ./sesion.js.
Error 3: usar ./ para un paquete de npm o al revés.
require('express') busca en node_modules; require('./express') busca un fichero. Son cosas distintas.
Error 4: creer que cada require crea una instancia nueva.
Es un singleton cacheado. Si necesitas instancias independientes, exporta la clase o una fábrica.
Error 5: mutar un objeto exportado por otro módulo.
Como todos comparten el mismo objeto, el cambio afecta a toda la aplicación. Object.freeze para la configuración, y copias defensivas para los datos.
Error 6: dependencias circulares. Uno de los dos módulos recibirá un objeto incompleto y el error será incomprensible. Extrae lo común a un tercer módulo o usa eventos.
Error 7: require dentro de un bucle o de una función caliente.
La caché lo hace barato tras la primera vez, pero la búsqueda en la caché no es gratis. Al principio del fichero.
Error 8: un utiles.js que lo tiene todo.
Acaba siendo un módulo del que depende todo el proyecto y que nadie se atreve a tocar.
Consejo 1: un module.exports único al final del fichero. Ese es el índice de la API pública de tu módulo.
Consejo 2: usa node: para los módulos del núcleo. require('node:fs'), require('node:path'), require('node:events').
Consejo 3: usa __dirname para construir rutas. Nunca dependas de process.cwd().
Consejo 4: dibuja el grafo de dependencias de tu proyecto. Si tiene ciclos o si un módulo tiene quince flechas entrantes, ya sabes dónde está el problema de diseño.
Ejercicios
Ejercicio 1: diagnosticar un módulo roto
Este módulo tiene cinco problemas según lo aprendido. Encuéntralos, explica la consecuencia de cada uno y reescríbelo.
// src/utiles/ayudas.js
const fs = require('fs');
console.log('Cargando ayudas...');
const catalogo = JSON.parse(fs.readFileSync('./datos/eventos.json', 'utf8'));
var CONFIG = { limite: 10 };
function formatearPrecio(c) {
return (c / 100).toFixed(2) + ' EUR';
}
function _redondear(n) {
return Math.round(n);
}
exports = { formatearPrecio, _redondear, CONFIG, catalogo };Ejercicio 2: el módulo de informes
Crea src/informes/ocupacion.js, un módulo que:
- Dependa solo de
../dominioy../utiles/formato.js(no decatalogo-datos.js: los datos se le pasan). - Exporte tres funciones:
resumirPorSala(eventos)→ array de{ sala, eventos, sesiones, aforo, vendidas, libres, ocupacion, recaudacionEuros }.sesionesEnRiesgo(eventos, umbralPorcentaje)→ sesiones por debajo del umbral de ocupación.sesionesAgotadas(eventos)→ sesiones sin entradas libres.
- Sea ejecutable directamente con
node src/informes/ocupacion.jsgracias arequire.main === module, cargando el catálogo y mostrando los tres informes conconsole.table. - Al ser importado con
require, no imprima absolutamente nada.
Verifica ambos comportamientos: la ejecución directa y un fichero que lo importe y solo llame a sesionesAgotadas.
Ejercicio 3: caché de módulos y dependencia circular
Escribe dos programas de laboratorio que demuestren experimentalmente lo aprendido:
Parte A — src/laboratorio/demostrar-cache.js:
- Un módulo
src/laboratorio/registro-ventas.jsque mantenga un array privado de ventas, exporteanotar(venta),total()ylistar(), e imprima un mensaje al ser evaluado. - Un programa que lo requiera desde tres puntos distintos (el propio programa y dos módulos auxiliares), anote ventas desde cada uno y demuestre que el total es compartido.
- Que después vacíe la caché con
delete require.cache[require.resolve(...)], lo vuelva a requerir y demuestre que el estado se ha perdido. - Que imprima cuántos módulos hay en
require.cacheantes y después.
Parte B — src/laboratorio/circular-*.js:
Crea una dependencia circular real entre un módulo pedido.js y un módulo entrada.js (un pedido tiene entradas; una entrada conoce su pedido), demuestra el fallo con una traza de mensajes, y después arréglala con la técnica que consideres correcta, justificando la elección.
Soluciones
Solución 1
Los cinco problemas:
| # | Problema | Consecuencia |
|---|---|---|
| 1 | exports = { ... } en lugar de module.exports |
El módulo no exporta nada. Quien lo importe recibe {} |
| 2 | Efecto secundario en la carga: lee un fichero y hace console.log |
Requerir el módulo bloquea el hilo y ensucia la salida sin que nadie lo pida |
| 3 | Ruta relativa a process.cwd() ('./datos/eventos.json') |
Falla si se ejecuta desde otra carpeta. Debe usar __dirname |
| 4 | Exporta detalles internos (_redondear, CONFIG, catalogo) |
Amplía el contrato público con cosas que deberían ser privadas, y CONFIG es mutable globalmente |
| 5 | var y require('fs') sin prefijo node: |
var no tiene ámbito de bloque; el prefijo evita ambigüedad con paquetes |
Y un sexto de propina: el módulo mezcla responsabilidades (formateo, configuración y acceso a datos). Deberían ser tres módulos.
Versión corregida, dividida como corresponde:
// src/utiles/formato.js
// Solo formateo. Sin dependencias, sin efectos secundarios.
function formatearPrecio(centimos) {
const euros = Math.floor(centimos / 100);
const resto = String(centimos % 100).padStart(2, '0');
return `${euros},${resto} EUR`;
}
module.exports = { formatearPrecio };// src/config.js
// Configuracion de la aplicacion. Congelada para que nadie la modifique.
module.exports = Object.freeze({
limiteEntradasPorPedido: 10,
umbralAforoBajo: 0.10
});// src/catalogo-datos.js
// Acceso a los datos. La lectura ocurre cuando se PIDE, no al cargar.
const fs = require('node:fs');
const path = require('node:path');
// Ruta relativa AL FICHERO, no al directorio de trabajo.
const RUTA_DATOS = path.join(__dirname, '..', 'datos', 'eventos.json');
function obtenerCatalogo() {
const contenido = fs.readFileSync(RUTA_DATOS, 'utf8');
return JSON.parse(contenido);
}
module.exports = { obtenerCatalogo };(La versión asíncrona de esa lectura llega en el Módulo 3; aquí lo que importa es que la lectura está dentro de una función.)
Solución 2
// src/informes/ocupacion.js
// Informes de ocupacion de Escena Viva.
// Recibe los eventos como argumento: no conoce su origen.
const { formatearPrecio } = require('../utiles/formato.js');
// Resumen agregado por sala.
function resumirPorSala(eventos) {
const porSala = new Map();
for (const evento of eventos) {
const acumulado = porSala.get(evento.sala) ?? {
sala: evento.sala, eventos: 0, sesiones: 0,
aforo: 0, vendidas: 0, recaudacionCentimos: 0
};
acumulado.eventos += 1;
acumulado.sesiones += evento.numeroSesiones;
acumulado.aforo += evento.aforoTotal;
acumulado.vendidas += evento.entradasVendidas;
acumulado.recaudacionCentimos += evento.recaudacionCentimos;
porSala.set(evento.sala, acumulado);
}
return [...porSala.values()].map((a) => ({
sala: a.sala,
eventos: a.eventos,
sesiones: a.sesiones,
aforo: a.aforo,
vendidas: a.vendidas,
libres: a.aforo - a.vendidas,
ocupacion: `${Math.round((a.vendidas / a.aforo) * 100)}%`,
recaudacionEuros: formatearPrecio(a.recaudacionCentimos)
}));
}
// Sesiones por debajo del umbral de ocupacion.
function sesionesEnRiesgo(eventos, umbralPorcentaje = 20) {
return eventos.flatMap((evento) =>
evento.sesiones
.filter((sesion) => sesion.ocupacion < umbralPorcentaje)
.map((sesion) => ({
evento: evento.id,
titulo: evento.titulo,
sesion: sesion.id,
libres: sesion.libres,
ocupacion: `${sesion.ocupacion}%`
}))
);
}
// Sesiones sin entradas disponibles.
function sesionesAgotadas(eventos) {
return eventos.flatMap((evento) =>
evento.sesiones
.filter((sesion) => sesion.agotada)
.map((sesion) => ({
evento: evento.id,
titulo: evento.titulo,
sesion: sesion.id,
aforo: sesion.aforo,
recaudacionEuros: formatearPrecio(sesion.recaudacionCentimos)
}))
);
}
// --- Ejecucion directa: solo si este fichero ES el programa principal ---
if (require.main === module) {
const { obtenerCatalogo } = require('../catalogo-datos.js');
const { Evento } = require('../dominio');
const eventos = obtenerCatalogo().map((datos) => Evento.desdeJSON(datos));
const umbral = Number(process.argv[2]) || 20;
console.log('OCUPACION POR SALA');
console.table(resumirPorSala(eventos));
console.log('');
console.log(`SESIONES EN RIESGO (menos del ${umbral}%)`);
const riesgo = sesionesEnRiesgo(eventos, umbral);
if (riesgo.length === 0) {
console.log(' Ninguna.');
} else {
console.table(riesgo);
process.exitCode = 1;
}
console.log('');
console.log('SESIONES AGOTADAS');
const agotadas = sesionesAgotadas(eventos);
if (agotadas.length === 0) {
console.log(' Ninguna.');
} else {
console.table(agotadas);
}
}
module.exports = { resumirPorSala, sesionesEnRiesgo, sesionesAgotadas };OCUPACION POR SALA ┌─────────┬────────────────────┬─────────┬──────────┬───────┬──────────┬────────┬───────────┬──────────────────┐ │ (index) │ sala │ eventos │ sesiones │ aforo │ vendidas │ libres │ ocupacion │ recaudacionEuros │ ├─────────┼────────────────────┼─────────┼──────────┼───────┼──────────┼────────┼───────────┼──────────────────┤ │ 0 │ 'Teatro Almendra' │ 1 │ 2 │ 840 │ 276 │ 564 │ '33%' │ '6612,00 EUR' │ │ 1 │ 'Sala Boveda' │ 1 │ 3 │ 360 │ 175 │ 185 │ '49%' │ '3114,00 EUR' │ │ 2 │ 'Auditorio Ribera' │ 1 │ 2 │ 1800 │ 1360 │ 440 │ '76%' │ '52572,00 EUR' │ └─────────┴────────────────────┴─────────┴──────────┴───────┴──────────┴────────┴───────────┴──────────────────┘ SESIONES EN RIESGO (menos del 20%) ┌─────────┬───────────┬──────────────────────┬─────────────┬────────┬───────────┐ │ (index) │ evento │ titulo │ sesion │ libres │ ocupacion │ ├─────────┼───────────┼──────────────────────┼─────────────┼────────┼───────────┤ │ 0 │ 'evt-002' │ 'Noche de Monologos' │ 'ses-002-3' │ 108 │ '10%' │ └─────────┴───────────┴──────────────────────┴─────────────┴────────┴───────────┘ SESIONES AGOTADAS Ninguna.
Y la comprobación de que importarlo no imprime nada:
// src/laboratorio/probar-informes.js
const { sesionesAgotadas } = require('../informes/ocupacion.js');
const { obtenerCatalogo } = require('../catalogo-datos.js');
const { Evento } = require('../dominio');
const eventos = obtenerCatalogo().map((d) => Evento.desdeJSON(d));
console.log(`Agotadas: ${sesionesAgotadas(eventos).length}`);Solo esa línea. Ni las tablas ni los encabezados aparecen, porque require.main !== module. Esa es la diferencia entre un módulo bien diseñado y uno con efectos secundarios.
Solución 3, parte A
// src/laboratorio/registro-ventas.js
console.error('>> registro-ventas.js EVALUANDO (esto solo debe verse una vez)');
const ventas = [];
function anotar(venta) {
ventas.push({ ...venta, anotadaEn: new Date().toISOString() });
return ventas.length;
}
function total() {
return ventas.reduce((t, v) => t + v.importeCentimos, 0);
}
function listar() {
return [...ventas];
}
module.exports = { anotar, total, listar };// src/laboratorio/taquilla-a.js
const registro = require('./registro-ventas.js');
function venderDesdeTaquillaA() {
registro.anotar({ sesionId: 'ses-001-1', cantidad: 2, importeCentimos: 5000 });
}
module.exports = { venderDesdeTaquillaA };// src/laboratorio/taquilla-b.js
const registro = require('./registro-ventas.js');
function venderDesdeTaquillaB() {
registro.anotar({ sesionId: 'ses-002-2', cantidad: 3, importeCentimos: 5400 });
}
module.exports = { venderDesdeTaquillaB };// src/laboratorio/demostrar-cache.js
console.error(`Modulos en cache al empezar: ${Object.keys(require.cache).length}`);
const registro = require('./registro-ventas.js');
const { venderDesdeTaquillaA } = require('./taquilla-a.js');
const { venderDesdeTaquillaB } = require('./taquilla-b.js');
console.error(`Modulos en cache tras los require: ${Object.keys(require.cache).length}`);
console.error('');
// Ventas desde tres puntos distintos del programa.
registro.anotar({ sesionId: 'ses-003-1', cantidad: 1, importeCentimos: 3800 });
venderDesdeTaquillaA();
venderDesdeTaquillaB();
console.log('--- Estado compartido ---');
console.log(`Ventas anotadas: ${registro.listar().length}`);
console.log(`Total: ${(registro.total() / 100).toFixed(2)} EUR`);
// Comprobacion de identidad.
const otraReferencia = require('./registro-ventas.js');
console.log(`¿Mismo objeto? ${registro === otraReferencia}`);
console.log(`Total visto desde la otra referencia: ${(otraReferencia.total() / 100).toFixed(2)} EUR`);
// --- Vaciado de la cache ---
console.log('');
console.log('--- Tras vaciar la cache ---');
const ruta = require.resolve('./registro-ventas.js');
delete require.cache[ruta];
const registroNuevo = require('./registro-ventas.js'); // Vuelve a evaluarse
console.log(`¿Mismo objeto que antes? ${registro === registroNuevo}`);
console.log(`Ventas en la instancia nueva: ${registroNuevo.listar().length}`);
console.log(`Ventas en la instancia antigua: ${registro.listar().length}`);
console.error('');
console.error(`Modulos en cache al terminar: ${Object.keys(require.cache).length}`);Modulos en cache al empezar: 1 >> registro-ventas.js EVALUANDO (esto solo debe verse una vez) Modulos en cache tras los require: 4 --- Estado compartido --- Ventas anotadas: 3 Total: 142.00 EUR ¿Mismo objeto? true Total visto desde la otra referencia: 142.00 EUR --- Tras vaciar la cache --- >> registro-ventas.js EVALUANDO (esto solo debe verse una vez) ¿Mismo objeto que antes? false Ventas en la instancia nueva: 0 Ventas en la instancia antigua: 3 Modulos en cache al terminar: 4
Tres conclusiones que se leen directamente en la salida:
- El mensaje de evaluación aparece una sola vez pese a los cuatro
require, y las tres ventas anotadas desde puntos distintos suman en el mismo array. Es el singleton. - Tras vaciar la caché, el módulo se vuelve a evaluar y la instancia nueva empieza de cero, mientras la antigua conserva su estado. Coexisten dos copias del mismo módulo, cada una con sus datos.
- Esa última frase es exactamente la razón por la que manipular
require.cacheen producción es peligroso: puedes acabar con dos "registros de ventas" y ventas repartidas entre ambos sin que nadie se dé cuenta.
Solución 3, parte B
La dependencia circular:
// src/laboratorio/circular-pedido.js
console.error('pedido.js: empieza');
const { Entrada } = require('./circular-entrada.js');
console.error('pedido.js: Entrada vale', typeof Entrada);
class Pedido {
constructor(id, sesionId, cantidad) {
this.id = id;
this.entradas = [];
for (let i = 0; i < cantidad; i++) {
this.entradas.push(new Entrada(`EV-2026-00000${i + 1}`, sesionId, this));
}
}
}
module.exports = { Pedido };
console.error('pedido.js: termina');// src/laboratorio/circular-entrada.js
console.error('entrada.js: empieza');
const { Pedido } = require('./circular-pedido.js');
console.error('entrada.js: Pedido vale', typeof Pedido); // <-- undefined
class Entrada {
constructor(codigo, sesionId, pedido) {
this.codigo = codigo;
this.sesionId = sesionId;
this.pedido = pedido;
this.estado = 'valida';
}
// Usa Pedido para validar: fallara si Pedido es undefined.
perteneceAPedidoValido() {
return this.pedido instanceof Pedido;
}
}
module.exports = { Entrada };
console.error('entrada.js: termina');pedido.js: empieza entrada.js: empieza entrada.js: Pedido vale undefined <-- el sintoma entrada.js: termina pedido.js: Entrada vale function pedido.js: termina
Y al usarlo: TypeError: Right-hand side of 'instanceof' is not callable.
El arreglo elegido: invertir la dependencia.
// src/dominio/entrada.js
// Entrada NO necesita conocer la clase Pedido: le basta con su identificador.
class Entrada {
constructor({ codigo, sesionId, pedidoId, estado = 'valida' }) {
this.codigo = codigo;
this.sesionId = sesionId;
this.pedidoId = pedidoId; // Solo el id, no el objeto
this.estado = estado;
}
usar() {
if (this.estado !== 'valida') {
const error = new Error(`La entrada ${this.codigo} esta ${this.estado}`);
error.codigo = 'ENTRADA_NO_VALIDA';
throw error;
}
this.estado = 'usada';
return this;
}
anular() {
this.estado = 'anulada';
return this;
}
}
module.exports = { Entrada };// src/dominio/pedido.js
// Pedido conoce a Entrada. Entrada no conoce a Pedido. Sin ciclo.
const { Entrada } = require('./entrada.js');
const { generarCodigoEntrada } = require('../utiles/formato.js');
class Pedido {
#entradas = [];
constructor({ id, usuarioId, sesionId, cantidad, precioCentimos, estado = 'pendiente' }) {
this.id = id;
this.usuarioId = usuarioId;
this.sesionId = sesionId;
this.cantidad = cantidad;
this.totalCentimos = cantidad * precioCentimos;
this.estado = estado;
}
get entradas() {
return [...this.#entradas];
}
emitir(anio, secuenciaInicial) {
if (this.estado !== 'pagado') {
const error = new Error(`No se pueden emitir entradas de un pedido ${this.estado}`);
error.codigo = 'ESTADO_INVALIDO';
throw error;
}
for (let i = 0; i < this.cantidad; i++) {
this.#entradas.push(new Entrada({
codigo: generarCodigoEntrada(anio, secuenciaInicial + i),
sesionId: this.sesionId,
pedidoId: this.id
}));
}
this.estado = 'emitido';
return this.entradas;
}
}
module.exports = { Pedido };Justificación de la elección. De las cuatro técnicas del apartado 7, invertir la dependencia es la correcta aquí porque el ciclo era un síntoma de un modelado incorrecto, no un problema técnico: una entrada no necesita el objeto Pedido completo, solo su identificador (pedidoId), que es exactamente lo que ya define el modelo de dominio del Módulo 1. Mover el require dentro del método habría funcionado, pero habría dejado el acoplamiento intacto y escondido; extraer a un tercer módulo habría añadido un fichero sin necesidad. El ciclo desapareció porque el diseño mejoró, que es siempre la mejor solución posible a una dependencia circular.
Conclusión
Has cerrado la deuda que arrastrábamos desde la tercera lección del curso. Ahora sabes que cada fichero de Node es un módulo con ámbito propio, y que esa privacidad por defecto —la que permite cambiar los detalles internos sin miedo— se consigue con un truco muy concreto: el envoltorio de módulo, una función que Node añade alrededor de tu código y que le inyecta cinco parámetros: exports, require, module, __filename y __dirname.
Entiendes con precisión la trampa clásica: exports y module.exports apuntan al mismo objeto al principio, así que añadir propiedades a cualquiera de los dos funciona, pero reasignar exports rompe la conexión y deja el módulo exportando un objeto vacío, porque require devuelve module.exports. De ahí la regla que seguiremos siempre: un único module.exports = { ... } al final del fichero, que además funciona como índice de la API pública.
Conoces el algoritmo de resolución: primero módulos del núcleo (mejor con el prefijo node:), después rutas relativas o absolutas con su cascada de extensiones y su interpretación como carpeta con index.js, y por último la búsqueda ascendente en node_modules. Y sabes que un módulo se evalúa una sola vez: es un singleton cacheado por ruta resuelta, lo que es perfecto para un grupo de conexiones o un registrador, y peligroso para configuración mutable o para pruebas que se contaminan entre sí. Sabes también qué devuelve Node ante una dependencia circular —un objeto incompleto al segundo módulo— y que la solución casi siempre es de diseño, no de sintaxis.
Y Escena Viva ha dejado de ser un montón de ficheros con datos copiados. Ahora tiene src/utiles/formato.js con las funciones de presentación que estaban duplicadas, src/dominio/sesion.js y src/dominio/evento.js con las clases del Módulo 1 —y con Evento delegando la validación de aforo en Sesion, que es quien sabe hacerla—, src/dominio/gestor-ventas.js de la lección anterior, src/dominio/index.js como fachada, src/catalogo-datos.js con module.exports y una firma preparada para volverse asíncrona en el Módulo 3, y un src/catalogo.js que ya solo lee argumentos y presenta. Los totales siguen cuadrando: 3000 de aforo, 1811 vendidas, 1189 libres.
Queda una última pieza del módulo, y es una que cambia el paisaje. Todo lo que has aprendido hoy —require, module.exports, la caché, el envoltorio— es CommonJS, el sistema propio de Node. Pero JavaScript acabó teniendo un sistema de módulos estándar, definido en el lenguaje y compartido con el navegador: import y export. No es una sintaxis alternativa para lo mismo: es un modelo estático, que se resuelve antes de ejecutar nada, con reglas distintas sobre extensiones, sin __dirname, sin require… y con await de nivel superior. En la siguiente lección, Módulos ES e Interoperabilidad, verás las dos direcciones de la convivencia entre ambos sistemas, sus límites reales, y escribirás la versión ESM del dominio de Escena Viva lado a lado con la que acabas de terminar.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
