Dominá los Módulos ESM en Node.js con 'type': 'module'

DUGLAS MORENODUGLAS MORENO 👁 11

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 desarrolladora

Este ejemplo muestra:

  • export const para exportar bindings de nivel superior.
  • import { … } from './greetings.js' para importar solo esos Named Exports.
  • El sistema de módulos resuelve rutas relativas con .js gracias 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:

10

Si 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.0

Importar 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: /proyecto

Este 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 Mundo

El 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 de package.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: 1234

4. 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.js

index.js puede hacer import { foo } from './utils/helpers.js';

Resumen del flujo de trabajo típico

  1. Agregar "type": "module" a package.json para tratar archivos .js como ESM.
  2. Usar import/export en archivos .js (o .mjs).
  3. Para CommonJS heredado, mantener archivos .cjs o usar require() con createRequire(import.meta.url).
  4. 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 (.mjs para ESM puro, .cjs para CommonJS).
  • Evitar mezclar require/import a menos que uses createRequire explícitamente.
  • Aprovechar import.meta.url para referencias relativas al archivo.
  • Utilizar await en 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.assert para 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 * y export { 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.

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario