Un módulo en Node es simplemente un archivo, pero un archivo cargado bajo reglas que le otorgan su propio ámbito privado, su propia forma de declarar lo que comparte y una ruta definida para que otros archivos puedan incorporarlo. Esto suena simple, y en su mayor parte lo es, excepto que Node realmente soporta dos sistemas diferentes para esas reglas —CommonJS y ES Modules—, además de varias categorías distintas de módulos dependiendo de dónde provenga el código.
Esta página es el mapa antes del territorio: Conceptos Básicos de Módulos recorre el código funcional para ambos sistemas, y CommonJS (require) / ES Modules (import) profundizan en la sintaxis y el comportamiento de cada uno. Aquí, el objetivo es el modelo mental: qué distingue realmente un "tipo" de módulo y por qué importa cuál estás viendo.
Los módulos de Node se dividen en dos ejes independientes: el sistema que los carga (CommonJS o ES Modules) y su origen (core, local, de terceros, JSON o nativo/WASM), y ambos ejes cambian cómo se comporta realmente un módulo dado.
Por Qué Importa: La mayoría de los errores de "por qué no funciona esta importación" son en realidad "estos son dos sistemas de módulos diferentes, no dos formas de escribir lo mismo", y saber con qué sistema y origen estás tratando te dice qué es realmente posible.
Conceptos Clave:CommonJS (CJS), ES Modules (ESM), ámbito del módulo, módulos core, especificador de módulo, package.json"type".
Cuándo Usar Este Modelo: Al iniciar un nuevo proyecto y decidir un valor predeterminado, depurar require is not defined o Cannot use import statement outside a module, mantener código que mezcla módulos antiguos y nuevos, o publicar un paquete que debe servir a ambos tipos de consumidores.
Limitaciones / Compromisos: Dos sistemas significan una fricción de interoperabilidad real: carga síncrona vs. asíncrona, exportaciones estáticas vs. dinámicas, y un riesgo genuino de que una dependencia se cargue dos veces como "dos módulos diferentes" si se incorpora de ambas maneras.
Temas Relacionados: CommonJS require, ES Modules import, package.json"type" y exports, interoperabilidad CJS/ESM, el algoritmo de resolución de módulos.
Antes de que existiera cualquier sistema de módulos, cada archivo JavaScript en un programa compartía un ámbito global: dos archivos podían sobrescribir silenciosamente las variables del otro simplemente declarando x. El primer trabajo de un sistema de módulos es resolver eso: envolver cada archivo en su propio ámbito privado, para que nada se filtre dentro o fuera excepto lo que el archivo comparte explícitamente.
Node necesitaba esto desde el primer día (2009), años antes de que el propio JavaScript tuviera una respuesta oficial, por lo que adoptó CommonJS, un sistema síncrono basado en require() construido para código del lado del servidor, respaldado por el sistema de archivos. Cuando JavaScript estandarizó más tarde su propio sistema de módulos, ES Modules (import/export), Node tuvo que agregar soporte para un segundo sistema diferente junto al que ya tenía, razón por la cual Node hoy funciona con dos sistemas en lugar de uno.
Una forma sencilla de visualizar la diferencia: CommonJS trata a require() como entregar una nota a un asistente mientras ya estás trabajando —"ve a buscarme ese archivo"— y esperar allí mismo hasta que regrese, para que puedas llamarlo en cualquier lugar, incluso condicionalmente. ES Modules, en cambio, trata a import como enviar un manifiesto de envío antes de que comience el trabajo: cada dependencia se declara por adelantado, en un lugar fijo, antes de que se ejecute una sola línea del propio código del módulo.
// CJS: se incorpora dinámicamente, dondequiera que lo llamesconst math = require('./math.js');// ESM: declarado estáticamente, siempre en la parte superior, antes de que se ejecute este archivoimport * as math from './math.js';
require.cache, con clave por ruta de archivo resuelta
El propio grafo de módulos; reimportar el mismo especificador reutiliza la instancia
Por origen, un módulo puede provenir de cinco lugares diferentes, y ese origen —independientemente del sistema que lo cargue— determina cómo (o si) se instala:
Módulos core / integrados - compilados en el propio binario de Node (fs, http, path, …). Nada que instalar; el prefijo node: (node:fs) hace que la fuente sea inequívoca en cualquier sistema.
Módulos locales / de archivo - tus propios archivos de proyecto, resueltos por ruta relativa o absoluta.
Módulos de terceros (npm) - instalados en node_modules, localizados por el algoritmo de resolución de Node que sube por el árbol de directorios desde el archivo importador.
Módulos JSON - datos planos, no código - require() lee JSON implícitamente en CJS; ESM requiere un atributo de importación explícito with { type: 'json' }.
Complementos nativos / módulos WebAssembly - código máquina compilado (C++/Rust a través de N-API, o un binario .wasm) cargado a través de un mecanismo completamente diferente, exponiendo una interfaz con forma de JS sin ser JavaScript en sí mismo.
Qué sistema se aplica a un archivo .js local dado no es una conjetura: Node lo decide a partir del campo "type" de package.json ("module" vs. el valor predeterminado de CommonJS) y de la propia extensión del archivo (.mjs y .cjs siempre anulan "type", forzando ESM o CJS respectivamente, independientemente de la configuración del paquete). package.json "type" y exports cubre esa decisión en su totalidad; Algoritmo de Resolución de Módulos cubre cómo un especificador como 'lodash' o './utils.js' se convierte realmente en un archivo en disco.
Debido a que CJS y ESM son sistemas de carga genuinamente diferentes, no solo sintaxis diferente, mezclarlos tiene modos de falla reales en lugar de cosméticos:
Preocupación
CommonJS
ES Modules
Tree-shaking (empaquetadores)
Pobre - require() dinámico derrota el análisis estático
Fuerte - las importaciones estáticas son analizables antes de la ejecución
Carga condicional
Natural - require() es solo una llamada a función
Requiere import() dinámico, que devuelve una Promesa
await de nivel superior
No es posible - require es síncrono
Soportado nativamente
Riesgo de interoperabilidad
Cargar el mismo paquete a través de require e import puede instanciarlo dos veces - un verdadero "peligro de paquete dual" que rompe las comprobaciones instanceof y el estado singleton compartido
Mismo riesgo, desde la otra dirección
Ese peligro de paquete dual es el filo más afilado en toda esta área: una dependencia cargada una vez a través de require() y otra a través de import no está garantizada de ser la misma instancia de módulo, lo que rompe silenciosamente cualquier cosa que dependa de la igualdad de referencia o del estado compartido a nivel de módulo. Interoperabilidad CJS ↔ ESM cubre createRequire, las extensiones explícitas .mjs/.cjs y cómo evitarlo durante una migración.
Los complementos nativos y los módulos WebAssembly se encuentran fuera de toda esta comparación CJS/ESM: se cargan a través de sus propios enlaces en lugar de analizarse como código fuente de JavaScript, pero aun así se presentan a tu código como un objeto importado ordinario una vez cargados, por lo que vale la pena nombrarlos como una categoría, aunque esta página no profundiza en la construcción de uno.
Para código nuevo, la guía actual es inequívoca: por defecto, usa ES Modules a través de "type": "module" en package.json. CommonJS no está obsoleto y Node no tiene planes de eliminarlo —el ecosistema es demasiado grande para eso—, pero la estructura estática de ESM es lo que las herramientas modernas (empaquetadores, verificadores de tipos, node --experimental-strip-types) asumen cada vez más.
"CommonJS y ES Modules son solo dos sintaxis para lo mismo." Difieren en la semántica de carga, no en la ortografía: resolución de grafo síncrona vs. asíncrona, importaciones dinámicas vs. estáticas, valores copiados vs. enlaces en vivo.
"Un módulo de Node siempre proviene de npm." Muchos nunca tocan node_modules: los módulos core se envían dentro del binario de Node, y los archivos locales son módulos en el momento en que otro archivo los importa.
"ESM es el moderno, así que CommonJS va a desaparecer." CommonJS sigue siendo totalmente compatible sin planes de eliminación; ESM es el predeterminado recomendado para código nuevo, no un reemplazo que se implementa debajo de los paquetes existentes.
"Puedes mezclar libremente require e import en el mismo archivo." El sistema de un solo archivo se fija por su extensión y el campo "type" de su paquete: puedes unir los dos sistemas (createRequire, import() dinámico), pero no puedes declarar sentencias require y import de nivel superior en un solo archivo.
"Un archivo .json importado en un módulo es básicamente JavaScript." Son datos, no código: CJS lo lee implícitamente a través de require(), mientras que ESM requiere un atributo de importación explícito type: 'json', y ninguno de los sistemas lo ejecuta.
¿Qué hace exactamente que un archivo sea un "módulo" en Node?
Cualquier archivo cargado bajo las reglas de un sistema de módulos: se le da su propio ámbito, con una forma explícita de exportar valores y una ruta definida para que otros archivos lo importen. Los scripts de nivel superior que se ejecutan fuera de un sistema de módulos no obtienen ese aislamiento.
¿Por qué Node tiene dos sistemas de módulos en lugar de uno?
CommonJS existía años antes de que JavaScript estandarizara su propia sintaxis de módulos, por lo que Node se basó en él primero. Una vez que ES Modules se convirtió en el sistema oficial del lenguaje, Node agregó soporte para él también, en lugar de romper el enorme ecosistema CommonJS existente al cambiar por completo.
¿Son los módulos core como `fs` y `http` un "tipo" diferente de mis propios archivos?
Sí, por origen: los módulos core se compilan en el binario de Node y no necesitan instalación, a diferencia de los archivos locales o los paquetes npm. Todavía se cargan a través del sistema (CJS o ESM) que utilice tu archivo importador.
¿Cómo decide Node si un archivo `.js` es CommonJS o ESM?
Primero verifica la extensión del archivo: .mjs siempre significa ESM, .cjs siempre significa CommonJS, independientemente de cualquier otra cosa. Para archivos .js simples, busca el campo "type" del package.json más cercano, por defecto CommonJS si ese campo está ausente.
¿Puede un solo archivo usar tanto `require` como `import`?
No, el sistema de módulos de un archivo se fija por su extensión/"type", y cada sistema solo reconoce su propia sintaxis. Puedes establecer un puente entre ellos (createRequire para obtener require dentro de un archivo ESM, o import() dinámico dentro de uno CJS), pero no puedes mezclar las formas estáticas import/export y require/module.exports en el mismo archivo.
¿Qué es un módulo JSON y por qué necesita una sintaxis especial en ESM?
Es un archivo de datos simple (.json) tratado como un módulo importable en lugar de código ejecutable. CommonJS lo lee implícitamente a través de require(); ESM requiere un atributo de importación explícito (with { type: 'json' }) porque, a diferencia de CJS, ESM necesita saber el tipo de contenido de un especificador antes de poder decidir cómo analizarlo.
¿Son los complementos nativos y los archivos WebAssembly realmente "módulos"?
Se comportan como uno desde la perspectiva de tu código —los importas/requieres y obtienes un objeto con forma de JS—, pero se cargan a través de su propio mecanismo de enlace en lugar de analizarse como código fuente de JavaScript, por lo que se clasifican por separado de CJS/ESM.
¿Cuál es el "peligro de paquete dual" al que debo prestar atención?
Es cuando el mismo paquete se carga una vez a través de require() y otra a través de import, produciendo dos instancias de módulo separadas en lugar de una compartida, lo que rompe silenciosamente las comprobaciones instanceof y cualquier estado que el módulo esperaba que fuera un singleton. Interoperabilidad CJS ↔ ESM cubre cómo evitarlo.
¿Debería un nuevo proyecto usar CommonJS o ES Modules?
ES Modules, a través de "type": "module" en package.json. Es el sistema estándar del lenguaje, tiene un soporte de herramientas más sólido (tree-shaking, análisis estático, await de nivel superior) y es la dirección en la que el ecosistema se está moviendo activamente; CommonJS sigue siendo totalmente compatible para el código existente y heredado.
¿Por qué el tree-shaking funciona mejor con ES Modules?
Los empaquetadores solo pueden eliminar de forma segura el código no utilizado cuando pueden probar, estáticamente, lo que un módulo importa y exporta, lo que garantizan las declaraciones import/export fijas al principio del archivo de ESM. require() de CommonJS es una llamada a función en tiempo de ejecución, por lo que un empaquetador no siempre puede saber lo que carga sin ejecutar realmente el código.
¿`import()` (la forma de función) pertenece a ESM o CommonJS?
Ambos pueden usarlo: import() dinámico es una función que devuelve una Promesa, disponible incluso dentro de archivos CommonJS, específicamente para que cualquiera de los sistemas pueda cargar un módulo de forma condicional o asíncrona sin necesidad de una declaración import estática.