CAP 10 · LEC 04·Módulos

ESM vs CommonJS: los dos sistemas de módulos en Node.js

Node.js soporta dos sistemas de módulos: CommonJS (el original, con require/module.exports) y ESM (el estándar web, con import/export). Entender sus diferencias y cómo interoperar es esencial para cualquier proyecto Node.js serio.

● AVANZADO9 min lectura3 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

CommonJS — el sistema original de Node.js

CommonJS fue diseñado por la comunidad Node.js en 2009 antes de que existiera un estándar de módulos en JavaScript. Es síncrono y todavía domina el ecosistema de paquetes npm.

// utils.cjs — módulo CommonJS const path = require("path"); // require es síncrono function resolverRuta(base, relativa) { return path.resolve(base, relativa); } function formatearBytes(bytes) { const unidades = ["B", "KB", "MB", "GB"]; let i = 0; while (bytes >= 1024 && i < unidades.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(2)} ${unidades[i]}`; } // Una sola exportación: module.exports es un objeto module.exports = { resolverRuta, formatearBytes }; // También puedes asignar directamente // module.exports = resolverRuta; // solo exporta la función // --- Uso en otro archivo --- const { resolverRuta, formatearBytes } = require("./utils.cjs"); console.log(formatearBytes(1536)); // "1.50 KB"
// tsconfig.json: "module": "commonjs" // TypeScript compila import/export → require/module.exports // utils.ts — escribes ESM syntax export function resolverRuta(base: string, relativa: string): string { const path = require("path"); return path.resolve(base, relativa); } export function formatearBytes(bytes: number): string { const unidades = ["B", "KB", "MB", "GB"]; let i = 0; while (bytes >= 1024 && i < unidades.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(2)} ${unidades[i]}`; } // TypeScript compila esto a: // Object.defineProperty(exports, "__esModule", { value: true }); // exports.formatearBytes = formatearBytes; // exports.resolverRuta = resolverRuta;
Salida1.50 KB

ESM — el estándar moderno

ES Modules es el sistema oficial de JavaScript desde ES2015. Es estático (analizable en tiempo de compilación), asíncrono, y habilita tree-shaking real.

// utils.mjs — módulo ESM puro import { resolve } from "node:path"; // prefijo node: recomendado en ESM export function resolverRuta(base, relativa) { return resolve(base, relativa); } export function formatearBytes(bytes) { const unidades = ["B", "KB", "MB", "GB"]; let i = 0; while (bytes >= 1024 && i < unidades.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(2)} ${unidades[i]}`; } // ESM tiene acceso a variables especiales equivalentes a CJS // __filename y __dirname NO existen en ESM — usar: import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Top-level await — SOLO disponible en ESM const config = await fetch("/api/config").then(r => r.json()); console.log(config);
// tsconfig.json: "module": "NodeNext" o "ESNext" // package.json: "type": "module" import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; export function resolverRuta(base: string, relativa: string): string { return resolve(base, relativa); } export function formatearBytes(bytes: number): string { const unidades = ["B", "KB", "MB", "GB"]; let i = 0; while (bytes >= 1024 && i < unidades.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(2)} ${unidades[i]}`; } const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Top-level await en ESM const VERSION = await fetch("https://api.ejemplo.com/version") .then(r => r.text()) .catch(() => "desconocida");

Diferencias clave

AspectoCommonJS (CJS)ESM
Sintaxis`require()` / `module.exports``import` / `export`
EvaluaciónSíncrona — bloquea hasta cargarAsíncrona — no bloquea
AnálisisDinámico — en runtimeEstático — en parse time
Top-level awaitNo disponibleSí, disponible
Tree-shakingLimitado — bundlers lo infierenNativo — exportaciones estáticas
`__dirname`Variable global disponibleUsar `import.meta.url`
Extensión por defecto`.js` (con `type` ausente)`.js` (con `"type": "module"`)
Ecosistema npmMayoría de paquetesCreciente, muchos puros-ESM
Tree-shaking: por qué importa

Con ESM, el bundler sabe exactamente qué exportaciones se usan antes de ejecutar el código. Puede eliminar el código no usado (dead code elimination). Con CJS, require puede llamarse condicionalmente y el bundler no puede garantizar qué se usa.

Interoperabilidad — cómo mezclar CJS y ESM

El escenario más común: tu código usa ESM pero una dependencia npm es solo CJS (o viceversa).

// ✅ ESM puede importar CJS con import estático // Node.js adapta module.exports como default export import lodash from "lodash"; // lodash es CJS const { chunk } = lodash; console.log(chunk([1, 2, 3, 4, 5], 2)); // [[1,2],[3,4],[5]] // ✅ ESM puede importar CJS con named imports (con advertencia) // Funciona para exports simples pero puede fallar con exports dinámicos import { chunk as chunk2 } from "lodash"; // ✅ CJS puede importar ESM con dynamic import (asíncrono) // Los módulos ESM puros NO se pueden importar con require() async function cargarModuloESM() { // require("./modulo.mjs") ← ❌ Error const modulo = await import("./modulo.mjs"); // ✅ return modulo; } // ❌ ESM NO puede importar JSON directamente (aún experimental) // import datos from "./config.json"; // Requiere --experimental-json-modules // ✅ Alternativa con fs import { readFileSync } from "node:fs"; const config = JSON.parse(readFileSync("./config.json", "utf8"));
// tsconfig.json para proyectos ESM con Node.js // { // "compilerOptions": { // "module": "NodeNext", // o "Node16" // "moduleResolution": "NodeNext", // "target": "ES2022", // "esModuleInterop": true, // permite import defaultName from 'cjs-module' // "allowSyntheticDefaultImports": true // } // } // Con esModuleInterop=true puedes importar CJS como default import lodash from "lodash"; // TypeScript añade el shim necesario import { chunk } from "lodash"; // También funciona // Para módulos sin tipos, instala @types/... // pnpm add -D @types/lodash // Importar y usar createRequire para CJS desde ESM import { createRequire } from "node:module"; const require = createRequire(import.meta.url); // Ahora puedes usar require en ESM para módulos que lo necesitan const paqueteCJS = require("./legacy-module.cjs");
Salida[[1,2],[3,4],[5]]

package.json — type y extensiones .mjs/.cjs

Node.js determina el sistema de módulos de cada archivo según estas reglas:

// package.json — "type" controla el default { "name": "mi-paquete", "version": "1.0.0", "type": "module", // Todos los .js son ESM // "type": "commonjs" // Todos los .js son CJS (default histórico) } // Las extensiones siempre tienen prioridad: // .mjs → siempre ESM, sin importar "type" // .cjs → siempre CJS, sin importar "type" // Publicar un paquete dual (CJS + ESM): // package.json con exports field: { "exports": { ".": { "import": "./dist/index.mjs", // Para ESM consumers "require": "./dist/index.cjs" // Para CJS consumers } }, "main": "./dist/index.cjs", // Fallback para Node.js antiguo "module": "./dist/index.mjs" // Hint para bundlers (Webpack, Vite) }
// Para proyectos ESM puros con TypeScript: // package.json // { // "type": "module", // "scripts": { "build": "tsc" } // } // tsconfig.json // { // "compilerOptions": { // "module": "NodeNext", // "moduleResolution": "NodeNext", // "outDir": "./dist" // } // } // IMPORTANTE: En TypeScript con NodeNext debes importar con extensión .js // aunque el archivo fuente sea .ts — TypeScript lo resuelve correctamente import { formatearBytes } from "./utils.js"; // ← .js aunque el fuente sea .ts // Para librerías duales, usa una herramienta como tsup: // tsup src/index.ts --format cjs,esm --dts // Genera: dist/index.js (CJS), dist/index.mjs (ESM), dist/index.d.ts console.log(formatearBytes(2048)); // "2.00 KB"
Salida2.00 KB

Practica