Menu

CommonJS vs ES Modules: require o import en Node

JavaScript convive con dos sistemas de módulos. Te cuento por qué existen los dos y cuándo usar require o import en tus proyectos Node.

Dos sistemas de módulos, un mismo lenguaje

Al principio, JavaScript no tenía ningún sistema de módulos. Node cubrió ese hueco en 2009 con CommonJS (require, module.exports), y durante años todo el código de Node se escribió así. Más tarde, en 2015, el propio lenguaje incorporó su sistema estándar: los ES Modules (import, export), que hoy funcionan tanto en los navegadores como en Node.

Por eso te vas a topar con los dos estilos en cualquier proyecto. Mira el mismo módulo escrito de las dos formas:

index.js
Output
Click Run to see the output here.
index.js
Output
Click Run to see the output here.

La misma función, dos envoltorios distintos. El resto de la página trata sobre cuándo importa cada envoltorio y cuál conviene usar.

Diferencias de sintaxis entre require e import

Las diferencias del día a día caben en una postal:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require es una llamada a función normal y corriente. import, en cambio, es una sentencia: solo puede aparecer en el nivel superior de un módulo y la ruta tiene que ser una cadena literal. Esta restricción no es un capricho del lenguaje; es justo lo que permite a los módulos ES hacer cosas que CommonJS no puede.

La diferencia real entre require e import: estático vs dinámico

CommonJS evalúa require() en el momento en que se ejecuta la línea. Puedes meterlo dentro de un if, calcular la ruta en tiempo de ejecución o cargar un módulo de forma condicional:

index.js
Output
Click Run to see the output here.

Los ES Modules son estáticos. El motor analiza todas las sentencias import antes de ejecutar una sola línea de código: arma el grafo de dependencias y resuelve todo por adelantado. Por eso la ruta tiene que ser una cadena literal y por eso import solo puede ir en el nivel superior del archivo.

¿La ventaja? Las herramientas pueden ver el grafo completo de módulos sin ejecutar nada. Así es como los bundlers hacen tree-shaking (eliminan los export que no se usan), los editores ofrecen autocompletado preciso y el navegador puede descargar módulos en paralelo.

Cuando de verdad necesitas carga dinámica en ESM, tienes import(): una expresión con forma de función que devuelve una Promise. A esto se le conoce como import dinámico:

index.js
Output
Click Run to see the output here.

Cómo decide Node qué sistema usa cada archivo

Un mismo proyecto de Node puede mezclar los dos tipos de archivos. Para saber qué sistema aplica a cada uno, Node se fija en dos cosas:

  • La extensión del archivo: .mjs siempre es ESM y .cjs siempre es CommonJS.
  • El campo "type" del package.json más cercano: si vale "module", los archivos .js se tratan como ESM; si vale "commonjs" (o directamente no aparece, que es lo que pasa por defecto), se tratan como CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

Con "type": "module", un hello.js normal dentro del mismo paquete usa import/export. Si colocas un hello.cjs al lado, ese archivo en concreto usará require. Así es como un proyecto puede migrar poco a poco, o cómo una librería puede publicar ambas variantes conviviendo.

Un tropiezo típico de principiante: dentro de un archivo ESM, require y module.exports sencillamente no existen. Si los escribes por pura inercia, te saltará un ReferenceError.

Interoperabilidad entre CommonJS y ES Modules

Tarde o temprano vas a necesitar que un archivo ESM consuma un paquete CommonJS, o al revés. Y las reglas no son simétricas.

Un ESM importando CommonJS funciona sin complicaciones. El objeto module.exports del módulo CJS se convierte en el export por defecto:

index.js
Output
Click Run to see the output here.
// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

Las importaciones con nombre desde CommonJS funcionan a veces (Node intenta detectar los named exports de forma estática), pero si quieres algo fiable, toma el default y desestructura:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS importando ESM es la dirección que duele. No puedes hacer require() de un módulo ES: te va a lanzar ERR_REQUIRE_ESM. La vía de escape es el import() dinámico, que también funciona desde CJS y devuelve una Promise:

index.js
Output
Click Run to see the output here.

El Node moderno (22+) incorporó un require() síncrono para ESM bajo ciertas condiciones, pero el import() dinámico sigue siendo la respuesta portable.

Otras diferencias de comportamiento que conviene conocer

Más allá de la sintaxis, ambos sistemas no se ponen de acuerdo en algunos detalles que, de vez en cuando, te hacen sudar:

index.js
Output
Click Run to see the output here.

Un par más:

  • this en el nivel superior. En CJS, this equivale a module.exports. En ESM es undefined, y además ESM siempre corre en modo estricto.
  • __dirname y __filename. En CJS vienen gratis. En ESM toca calcularlos a partir de import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • Extensiones de archivo en los imports. ESM exige la extensión ("./utils.js", no "./utils") en las rutas relativas. CJS es más permisivo.
  • Enlaces vivos vs. copias estáticas. Los imports de ESM son referencias vivas a las variables del módulo que exporta. CJS te entrega una copia de lo que sea que se haya asignado a module.exports al momento de cargarlo. La mayoría del código nunca nota la diferencia, pero sí importa cuando hay dependencias circulares.

¿Cuál deberías usar?

Para un proyecto nuevo: ES Modules. Pon "type": "module" en el package.json y no mires atrás. ESM es el estándar del lenguaje, funciona igual en el navegador y en Node, permite await a nivel superior y las herramientas modernas están pensadas para él.

Quédate con CommonJS cuando:

  • Mantienes una base de código CJS existente y migrar aún no compensa el esfuerzo.
  • Publicas una librería que tiene que dar soporte a versiones muy antiguas de Node o a consumidores que no pueden usar ESM.
  • Una dependencia crítica solo se distribuye en CJS y su interoperabilidad es un lío. (Hoy es raro, pero pasa.)

Aun así, vas a leer código ESM todo el tiempo: casi todo lo que se ha publicado en npm en los últimos años tira hacia ahí. Manejarte con ambos no es opcional; dominar los modismos del que realmente estás escribiendo, tampoco.

Checklist mental rápido

Cuando abras un archivo nuevo, pregúntate:

  • ¿Este archivo usa import/export o require/module.exports? No los mezcles.
  • ¿Qué dice el package.json más cercano sobre "type"?
  • Si vas a importar un paquete, revisa su package.json: ¿trae ESM, CJS o ambos?
  • Si te topas con ERR_REQUIRE_ESM, estás en CJS intentando cargar ESM. Cambia a import() dinámico o mueve el llamador a ESM.

El noventa por ciento de la confusión con módulos en Node cae en alguno de esos cuatro puntos.

Siguiente: lo básico de npm

Los módulos te sirven para dividir tu código entre varios archivos. El siguiente paso es traer código que escribieron otras personas: para eso está npm. Vamos a ver cómo instalar paquetes, los rangos de semver y las partes del flujo de npm que de verdad usas en el día a día.

Preguntas frecuentes

¿Cuál es la diferencia entre require e import en JavaScript?

require es la forma CommonJS de cargar módulos: es síncrono, se ejecuta justo en el punto donde lo llamas y devuelve lo que el módulo haya asignado a module.exports. import, en cambio, es la sintaxis de ES Modules: es estático, se eleva al inicio del archivo y se analiza antes de que el código corra. Además, no coinciden en qué es this, en cómo resuelven las dependencias circulares ni en si permiten await a nivel superior (solo ESM lo permite).

¿Qué conviene usar en un proyecto nuevo de Node, CommonJS o ES Modules?

Tira por ES Modules. Pon "type": "module" en tu package.json y escribe con import/export. ESM es el estándar oficial, funciona tanto en el navegador como en Node y soporta await en el nivel superior. CommonJS todavía aparece en paquetes y herramientas antiguas, así que aunque no lo escribas te vas a topar con él leyendo código ajeno.

¿Se pueden mezclar require e import en el mismo proyecto?

Sí, pero con reglas. Un archivo .mjs o un paquete con "type": "module" usa ESM; un .cjs o "type": "commonjs" usa CJS. Desde ESM puedes hacer import de un módulo CommonJS (su module.exports aparece como el export por defecto). Lo que no puedes es hacer require() de un módulo ESM desde CommonJS: en ese caso tienes que usar import() dinámico, que devuelve una Promise.

Aprende a programar con Coddy

COMENZAR