Dominá los Módulos ESM en Node.js con 'type': 'module'
Introducción
Node.js soporta dos sistemas de módulos: CommonJS, el estándar tradicional, y ESM (ECMAScript Modules), el estándar moderno de JavaScript. Si querés escribir código más limpio, aprovechar las sintaxis actuales de JavaScript y trabajar mejor con herramientas modernas (bundlers, frameworks y herramientas de empaquetado), tenés que dominar los módulos ESM. En este artículo, te guiaré paso a paso en el uso de ESM en Node.js, desde configurar package.json hasta escribir código de importación/exportación, con ejemplos prácticos y su salida esperada. Al finalizar, sabrás cuándo usar ESM, cómo interoperar con CommonJS y cuáles son las mejores prácticas que evitarán errores comunes.
Por qué usar ESM en Node.js hoy
Node.js adopted ECMAScript Modules (ESM) en versiones recientes (v12+), brindando soporte nativo a la sintaxis import/export. A diferencia de CommonJS, que usa require() y module.exports, los módulos ESM siguen la sintaxis declarativa del lenguaje, permiten Named Exports, Default Exports y son más fáciles de entender para desarrolladores familiarizados con frameworks modernos (React, Vue, Angular). Además, los módulos ESM son más rápidos en cargados asíncronos (sin overhead de require()),
apoyan top-level await, funcionan mejor con herramientas de bundlear (es decir Vite, Webpack 5) y son más predecibles al reexportar.
Si estás migrando un proyecto, querés crear una librería moderna o simplemente tenés curiosidad, dominar ESM te da mayor flexibilidad para organizar tu código y evitar problemas de circularidad típicos de CommonJS.
El papel de package.json y el campo "type"
El punto de entrada a ESM es un sencillo cambio en package.json. Agregando "type": "module" en la raíz, indicás a Node que los archivos .js (y .cjs para CommonJS) deben ser tratados como módulos ESM. Esto elimina la necesidad de usar extensiones .mjs (aunque podés usarlas). Con este cambio, podés usar import/export directamente en cualquier archivo .js.
{
"name": "ejemplo-esm",
"version": "1.0.0",
"description": "Demostración de ESM en Node.js",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"author": "Tu Nombre",
"license": "MIT"
}Una vez que guardás este archivo, todos los archivos .js en tu proyecto son automáticamente módulos ESM. Si tenés código heredado en CommonJS, podés mantenerlos usando archivos .cjs (o renombrándolos).
Ejemplo Básico: Exportar un valor simple e importarlo
Empecemos con un archivo greetings.js que exporta un string constante y un objeto. Luego, importaremos ambos en index.js y los mostraremos por consola.
greetings.js
// greetings.js
export const message = "¡Hola, mundo!";
export const user = {
name: "Ana",
role: "desarrolladora"
};index.js
// index.js
import { message, user } from './greetings.js';
console.log(message);
console.log(user.name, user.role);Salida esperada (al ejecutar node index.js):
¡Hola, mundo!
Ana desarrolladoraEste ejemplo muestra:
export constpara exportar bindings de nivel superior.import { … } from './greetings.js'para importar solo esos Named Exports.- El sistema de módulos resuelve rutas relativas con
.jsgracias a"type": "module".
Exportación por defecto (Default Export)
También podés exportar un solo valor usando export default. Util es para clases, funciones o configuraciones.
utils.js
// utils.js
export default function adder(a, b) {
return a + b;
}main.js
// main.js
import adder from './utils.js';
console.log(adder(3, 7));Salida:
10Si querés exportar default + Named exports simultáneamente, combinás ambas sintaxis:
// math.js
export default class Calculator {
multiply(x, y) { return x * y; }
}
export const version = '2.0';import Calculator, { version } from './math.js';
const calc = new Calculator();
console.log(calc.multiply(4,5), version);Salida:
20 2.0Importar todo como un Namespace (para paquetes de terceros)
Algunos paquetes (por ejemplo, lodash) solo expone Named Exports. Si querés todo el objeto, podés usar import * as _ from 'lodash';.
usandoLodash.js
import * as _ from 'lodash';
const arr = [1,2,3];
console.log(_.map(arr, x => x*2));Salida (requiere lodash instalado):
[ 2, 4, 6 ]Esto es útil para inspeccionar la API de un módulo o cuando necesitás pasar el módulo completo a otra función.
Usar import.meta.url para referencias relativas al archivo actual
import.meta.url es un string con la URL del módulo actual. Es muy útil al querer leer archivos locales (por ejemplo, ./data.json) o construir rutas relativas dentro de un módulo.
resolverRuta.js
import { resolve } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename);
console.log('Archivo actual:', __filename);
console.log('Directorio raíz:', __dirname);Salida (ejecutado desde /proyecto/resolverRuta.js):
Archivo actual: file:///proyecto/resolverRuta.js
Directorio raíz: /proyectoEste patrón se vuelve esencial cuando trabajás con file system (FS) o con herramientas que necesitan saber dónde está ubicado el módulo.
Interoperabilidad con CommonJS (require vs import)
Muchas bibliotecas aún usan CommonJS (module.exports). Los módulos ESM pueden interoperar con ellos usando import/require. Node.js permite mezclarlos, pero tenés que ser cuidadoso con la forma.
commonjsLib.cjs
// commonjsLib.cjs (no tiene "type": "module")
module.exports = {
greet: function(name) { return `Hola ${name}`; }
};importarCommonJS.js
import cjs from 'require-from-path'; // requerir un CommonJS desde ESM
// Usar import.meta.url para resolver la ruta al paquete
const mod = await import('module'); // importar dinámicamente el módulo 'module'
const { createRequire } = mod;
const require = createRequire(import.meta.url);
const lib = require('./commonjsLib.cjs');
console.log(lib.greet('Mundo'));Salida:
Hola MundoEl createRequire proporciona un require que entiende CommonJS, permitiéndonos cargar archivos .cjs desde un módulo ESM. Alternativamente, podés renombrar commonjsLib.cjs a commonjsLib.js y mantener la configuración en package.json para que siga siendo CommonJS.
Buenas prácticas y errores comunes
1. Extensiones de archivo adecuadas
.js→ Se comporta como ESM si"type": "module", de otra forma CommonJS..mjs→ Siempre ESM, sin necesidad depackage.json..cjs→ Siempre CommonJS, útil para código heredado.
Usar .mjs es la opción más limpia para nuevos módulos ESM, evitando confusión.
2. Evitar require() y module.exports en módulos ESM
Si usás require() accidentalmente, Node lo interpreta como una expresión de valor, no como un importador de módulos, lo que lleva a un undefined o a un comportamiento inesperado. Mejor, refactorizá para usar import/export.
3. Top-level await (ESM only)
Los módulos ESM permiten await en nivel superior, ideal para I/O asíncrono sin callbacks.
ejemploAwait.js
import { readFile } from 'fs/promises';
// Esperar hasta que el archivo sea leído
const content = await readFile('README.md', 'utf8');
console.log('Longitud del archivo:', content.length);Salida (si existe README.md):
Longitud del archivo: 12344. Exportaciones circularmente dependientes
Dado que los módulos ESM se resuelven al inicio, las importaciones circulares pueden causar inicializaciones parciales. Si surgieran, reestructurá utilizando factorización o un patrón de servicios compartidos.
5. Compatibilidad con módulos anidados
Si tenés una carpeta src con múltiples archivos .js, Node resolverá imports relativos entre ellos sin problemas.
/src
index.js
utils/
helpers.jsindex.js puede hacer import { foo } from './utils/helpers.js';
Resumen del flujo de trabajo típico
- Agregar
"type": "module"apackage.jsonpara tratar archivos.jscomo ESM. - Usar
import/exporten archivos.js(o.mjs). - Para CommonJS heredado, mantener archivos
.cjso usarrequire()concreateRequire(import.meta.url). - Probar con
node index.js(asegurándote que la versión de Node sea v14+ para ESM completo).
Conclusión
Dominar ESM en Node.js abre la puerta a patrones más limpios, mejores herramientas y un código más moderno. Con un cambio de un campo en package.json, podés escribir imports/export naturales, aprovechar las características asíncronas del lenguaje y trabajar con el ecosistema JavaScript actual. Recordá:
- Usar extensiones adecuadas (
.mjspara ESM puro,.cjspara CommonJS). - Evitar mezclar
require/importa menos que usescreateRequireexplícitamente. - Aprovechar
import.meta.urlpara referencias relativas al archivo. - Utilizar
awaiten nivel superior cuando trabajes con I/O asíncrono.
Ahora, podés migrar proyectos, construir librerías limpias y permanecer compatible con frameworks modernos que esperan módulos ESM. ¡Feliz programación!
Próximos pasos
- Explorar
import.assertpara importaciones dinámicas con opciones (por ejemplo, JSON, YAML). - Aprender sobre las nuevas importaciones con namespaces (
import * as). - Probar con bundlers (Vite, Webpack) para ver cómo optimizan módulos ESM.
- Experimentar con
export *yexport { default }para reexportar módulos. - Revisar la documentación oficial de Node sobre ECMAScript Modules para mantenerte al día con las últimas características.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.