Módulos CommonJS (Node.js)
Introducción
Node.js revolucionó el desarrollo en JavaScript al llevar el lenguaje fuera del navegador y permitir la creación de aplicaciones de servidor. Uno de los elementos fundamentales que hizo posible esto fue su sistema de módulos llamado CommonJS. Este sistema permite organizar el código JavaScript en piezas independientes y reutilizables, facilitando la gestión de dependencias y la creación de aplicaciones escalables.
En este artículo exploraremos el sistema de módulos CommonJS utilizado en Node.js, sus particularidades y cómo funciona. Aunque los módulos ES6 (que vimos en el artículo anterior) son cada vez más utilizados, el sistema CommonJS sigue siendo fundamental en el ecosistema de Node.js y es esencial comprender su funcionamiento para trabajar en proyectos del lado del servidor.
Sistema de módulos en Node.js
Node.js fue diseñado desde el principio con un enfoque modular, adoptando el estándar CommonJS para la gestión de módulos. Este enfoque proporciona varias ventajas:
- Encapsulamiento: Cada módulo tiene su propio ámbito, evitando conflictos de nombres y variables.
- Reutilización: El código puede ser fácilmente compartido entre diferentes partes de una aplicación.
- Organización: Permite estructurar el código en unidades lógicas y manejables.
- Gestión de dependencias: Facilita la incorporación de bibliotecas externas.
En Node.js, cada archivo JavaScript es tratado como un módulo independiente. Todo lo que se declara dentro de un archivo es privado por defecto, a menos que se exporte explícitamente.
Sintaxis require() y module.exports
Exportando módulos
En CommonJS, utilizamos module.exports
para exponer funcionalidades desde un módulo:
// matematicas.js
function sumar(a, b) {
return a + b;
}
function restar(a, b) {
return a - b;
}
// Exportamos las funciones para que sean accesibles desde otros archivos
module.exports = {
sumar: sumar,
restar: restar
};
En ES6+ podemos usar la sintaxis abreviada de objetos cuando el nombre de la propiedad y el valor son iguales:
// matematicas.js
function sumar(a, b) {
return a + b;
}
function restar(a, b) {
return a - b;
}
module.exports = { sumar, restar };
Importando módulos
Para utilizar un módulo exportado, usamos la función require()
:
// app.js
// Importamos el módulo matematicas
const matematicas = require('./matematicas');
console.log(matematicas.sumar(5, 3)); // 8
console.log(matematicas.restar(10, 4)); // 6
El argumento de require()
es la ruta relativa al archivo del módulo. Para los módulos propios de Node.js o instalados con npm, se utiliza directamente el nombre sin ruta:
// Módulo nativo de Node.js
const fs = require('fs');
// Módulo instalado con npm
const express = require('express');
Exportación de múltiples valores
Hay varias formas de exportar múltiples valores desde un módulo CommonJS:
1. Exportar un objeto con múltiples propiedades
Este es el enfoque más común, como vimos en el ejemplo anterior:
// utilidades.js
function validarEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function formatearFecha(fecha) {
return new Date(fecha).toLocaleDateString('es-ES');
}
const VERSION = '1.0.0';
module.exports = {
validarEmail,
formatearFecha,
VERSION
};
2. Exportar valores individuales
También podemos añadir propiedades directamente a module.exports
o al objeto exports
:
// utilidades.js
// Estas dos formas son equivalentes
exports.validarEmail = function(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
module.exports.formatearFecha = function(fecha) {
return new Date(fecha).toLocaleDateString('es-ES');
};
exports.VERSION = '1.0.0';
Importante: exports
es simplemente una referencia al objeto module.exports
. Si asignamos un nuevo valor a exports
, romperemos esta referencia:
// Esto NO funciona como se espera
exports = {
validarEmail: function(email) { /* ... */ }
};
// Esto SÍ funciona
module.exports = {
validarEmail: function(email) { /* ... */ }
};
Módulos nativos vs. instalados
Node.js incluye varios módulos nativos que proporcionan funcionalidades básicas y no requieren instalación adicional:
// Módulos nativos
const fs = require('fs'); // Sistema de archivos
const path = require('path'); // Manejo de rutas
const http = require('http'); // Servidor HTTP
const crypto = require('crypto'); // Funciones criptográficas
const os = require('os'); // Información del sistema operativo
Los módulos instalados vía npm se importan de manera similar, pero primero deben ser instalados:
npm install express
// Módulo instalado
const express = require('express');
Resolución de rutas de módulos
Node.js utiliza un algoritmo específico para resolver las rutas cuando utilizamos require()
:
1. Módulos nativos
Si el nombre coincide con un módulo nativo de Node.js (como 'fs' o 'http'), se carga ese módulo.
2. Módulos con ruta relativa o absoluta
Si la ruta comienza con ./
, ../
o /
, Node.js intenta cargar el archivo exacto especificado:
// Módulo en el mismo directorio
const miModulo = require('./mi-modulo');
// Módulo en el directorio padre
const otroModulo = require('../otro-modulo');
// Ruta absoluta
const configModulo = require('/ruta/absoluta/config');
Node.js busca en este orden:
- El archivo exacto (
mi-modulo.js
) - El archivo con extensión
.js
(mi-modulo.js
) - El archivo como directorio con un archivo
index.js
(mi-modulo/index.js
) - El archivo definido en el campo
main
delpackage.json
del directorio
3. Módulos de node_modules
Si la ruta no comienza con ./
, ../
o /
, Node.js busca el módulo en el directorio node_modules
:
// Busca primero en ./node_modules/express
// Luego en ../node_modules/express
// Y sigue subiendo hasta encontrarlo o llegar a la raíz del sistema
const express = require('express');
Caché de módulos
Node.js almacena en caché los módulos la primera vez que se cargan. Esto significa que si importamos el mismo módulo varias veces, obtendremos la misma instancia:
// archivo1.js
const modulo = require('./modulo');
modulo.contador = 1;
console.log(modulo.contador); // 1
// archivo2.js
const modulo = require('./modulo');
console.log(modulo.contador); // 1, no 0
modulo.contador++;
// archivo1.js (continuación)
console.log(modulo.contador); // 2
Este comportamiento es importante para entender cómo funcionan los estados compartidos en aplicaciones Node.js.
Interoperabilidad con ES modules
Desde Node.js 13.2.0, hay soporte nativo para módulos ES6, y existe cierta interoperabilidad entre los dos sistemas:
Importar módulos CommonJS desde ES modules
// Módulo CommonJS
// matematicas.js
module.exports = {
sumar(a, b) {
return a + b;
}
};
// Módulo ES6
// app.mjs
import matematicas from './matematicas.js';
console.log(matematicas.sumar(2, 3)); // 5
Importar ES modules desde CommonJS
La importación directa de módulos ES6 desde CommonJS no es posible con la sintaxis de require()
. Sin embargo, se puede usar importación dinámica:
// Módulo ES6
// utilidades.mjs
export function validarEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Módulo CommonJS
// app.js
async function main() {
const utilidades = await import('./utilidades.mjs');
console.log(utilidades.validarEmail('test@example.com')); // true
}
main();
Patrones comunes en Node.js
Patrón de módulo de configuración
Un patrón común es tener un módulo de configuración que centraliza todas las variables de configuración:
// config.js
module.exports = {
entorno: process.env.NODE_ENV || 'desarrollo',
puerto: process.env.PORT || 3000,
dbURL: process.env.DB_URL || 'mongodb://localhost:27017/miapp',
secretoJWT: process.env.JWT_SECRET || 'clave-secreta-desarrollo'
};
// app.js
const config = require('./config');
console.log(`Servidor iniciado en el puerto ${config.puerto}`);
Patrón de módulo de única responsabilidad
Cada módulo debería tener una única responsabilidad bien definida:
// db.js - Manejo de conexión a base de datos
const mongoose = require('mongoose');
const config = require('./config');
async function conectar() {
await mongoose.connect(config.dbURL);
console.log('Conectado a la base de datos');
}
module.exports = { conectar };
// server.js - Configuración del servidor HTTP
const express = require('express');
const config = require('./config');
function iniciar() {
const app = express();
return app.listen(config.puerto, () => {
console.log(`Servidor escuchando en el puerto ${config.puerto}`);
});
}
module.exports = { iniciar };
Patrón de fachada (facade)
Crear una interfaz simplificada para un conjunto de funcionalidades:
// logger.js
const fs = require('fs');
const path = require('path');
function info(mensaje) {
logGenerico('INFO', mensaje);
}
function error(mensaje) {
logGenerico('ERROR', mensaje);
}
function logGenerico(nivel, mensaje) {
const fecha = new Date().toISOString();
const entrada = `[${fecha}] [${nivel}] ${mensaje}\n`;
fs.appendFileSync(path.join(__dirname, 'app.log'), entrada);
console.log(entrada);
}
module.exports = { info, error };
// En otro archivo
const logger = require('./logger');
logger.info('Aplicación iniciada');
logger.error('Ocurrió un error');
Resumen
Los módulos CommonJS son un componente fundamental en el ecosistema de Node.js, proporcionando un sistema para dividir el código en unidades independientes y reutilizables. A través de require()
y module.exports
, podemos compartir funcionalidades entre diferentes archivos de manera sencilla y estructurada.
Aunque los módulos ES6 están ganando popularidad, CommonJS sigue siendo ampliamente utilizado en el entorno de Node.js. Comprender sus mecánicas, como la resolución de rutas y el comportamiento de caché, es esencial para desarrollar aplicaciones Node.js robustas y mantenibles. Además, la creciente interoperabilidad entre ambos sistemas permite una transición gradual hacia los módulos ES6, combinando lo mejor de ambos mundos según sea necesario.