El ecosistema de Node.js ha evolucionado significativamente en los últimos años, y uno de los cambios más disruptivos ha sido la adopción nativa de módulos ECMAScript (ESM) frente al tradicional CommonJS. Para muchos equipos de desarrollo, la transición de un sistema a otro puede convertirse en un laberinto de errores difíciles de depurar, especialmente cuando se combinan con configuraciones de herramientas modernas. En este artículo analizamos los siete errores reales más comunes que surgen al trabajar con el campo 'type': 'module' en package.json, y ofrecemos un mapa práctico de extensiones .cjs y .mjs para navegar la migración sin perder la cordura.

La regla fundamental que todo desarrollador debe memorizar es que el campo 'type' en el package.json más cercano determina cómo se interpretan los archivos .js. Si se define 'type': 'module', cada .js se trata como ESM; si no se especifica o se define como 'commonjs', se trata como CommonJS. Las extensiones .cjs y .mjs ignoran por completo ese campo: .cjs siempre es CommonJS y .mjs siempre es ESM. Esta simple tabla resuelve el 90% de los problemas: bajo 'type': 'module', foo.js es ESM, foo.mjs ESM, foo.cjs CJS. Bajo 'type': 'commonjs' (o ausente), foo.js es CJS, foo.mjs ESM, foo.cjs CJS.

El primer error que aparece al activar 'type': 'module' es el clásico ReferenceError: require is not defined in ES module scope. Ocurre cuando un archivo .js que antes usaba require() y module.exports ahora se interpreta como ESM. La solución más rápida —y la que recomendamos en entornos de producción— es renombrar ese archivo a .cjs en lugar de reescribir todo su contenido. Este simple cambio resolvió once de cuarenta y dos archivos en una migración real de una herramienta interna.

El segundo error es Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported. Sucede cuando desde un contexto CommonJS se intenta hacer require() a un módulo que ahora es ESM. La alternativa limpia es usar createRequire desde node:module para obtener una función require dentro de un archivo ESM. Este escape hatch es especialmente útil cuando se depende de librerías antiguas que aún no se han migrado.

El tercer error, aunque menos frecuente, es el SyntaxError: Cannot use import statement outside a module, que aparece cuando se ejecuta un archivo .js con import sin tener 'type': 'module'. La regla de extensiones también lo resuelve: basta con renombrar a .mjs o añadir el campo 'type' en el package.json correspondiente.

Para equipos que construyen librerías o aplicaciones que deben funcionar tanto en entornos CommonJS como ESM, la solución más robusta es el campo exports con condiciones. Un ejemplo clásico incluye las claves types, import y require en ese orden, y asegurarse de que el archivo para CommonJS tenga extensión .cjs. Herramientas como tsup facilitan este paso generando automáticamente los formatos duales. Un detalle que suele pasar desapercibido: siempre hay que exportar ./package.json para evitar errores como ERR_PACKAGE_PATH_NOT_EXPORTED en herramientas como Vite o Jest.

Hablando de Jest y ESLint, la migración suele tropezar con sus archivos de configuración. Jest, al día de hoy, carga su configuración como CommonJS, por lo que si tu proyecto tiene 'type': 'module', el archivo jest.config.js con module.exports fallará. La solución es renombrarlo a jest.config.cjs y, si además quieres que los tests ejecuten código ESM de verdad, debes añadir NODE_OPTIONS=--experimental-vm-modules en el script de test. En cambio, ESLint 9 con flat config espera que eslint.config.js sea ESM cuando el proyecto tiene 'type': 'module', por lo que debe usar export default. Tener ambos archivos —uno CJS y otro ESM— en el mismo repositorio es perfectamente válido y no debe percibirse como mala práctica.

La desaparición de __dirname y __filename en ESM es otra fuente común de roturas. La solución estándar es importar fileURLToPath y dirname de node:url y node:path, y reconstruir esas variables. Desde Node 21.2+ existe import.meta.dirname que simplifica el proceso, pero si tu rango de versiones soportadas incluye Node 20, es más seguro mantener el polyfill manual.

La experiencia acumulada tras migrar repositorios de decenas de archivos sugiere una estrategia conservadora: no actives 'type': 'module' de golpe en toda la base de código. En su lugar, migra archivo por archivo usando la extensión .mjs, convierte sus importaciones y ejecuta tests en cada paso. Solo al final, cuando todos los .js restantes sean ya ESM compatibles, activa el flag global. Para proyectos nuevos, haz lo contrario: define 'type': 'module' desde el primer commit y usa .cjs solo para los pocos archivos de configuración que lo requieran.

En Q2BSTUDIO, como empresa especializada en desarrollo de aplicaciones a medida, sabemos que la migración a ESM no es un mero ejercicio técnico, sino una decisión estratégica que afecta a la mantenibilidad, el rendimiento y la interoperabilidad con el ecosistema moderno. Nuestros equipos aplican este tipo de buenas prácticas en cada proyecto, ya sea en la creación de software a medida, en la integración de servicios cloud AWS y Azure o en el desarrollo de soluciones de inteligencia artificial para empresas. Por ejemplo, al construir agentes IA que interactúan con APIs modernas, la correcta configuración de módulos ESM evita costosos errores en tiempo de ejecución. Del mismo modo, en proyectos de ciberseguridad o de inteligencia de negocio con Power BI, la estandarización del sistema de módulos permite que las herramientas de análisis y visualización se integren sin fricción.

El camino hacia ESM tiene sus trampas, pero con una tabla de extensiones a mano, un puñado de createRequire y la paciencia de migrar progresivamente, los errores como ERR_REQUIRE_ESM dejan de ser un misterio. Para quienes buscan acompañamiento profesional en estos procesos, desde Q2BSTUDIO ofrecemos servicios de consultoría y desarrollo que abarcan desde la automatización de procesos hasta la implementación de servicios inteligencia de negocio, siempre con un enfoque práctico y orientado a resultados. La inteligencia artificial para empresas también se beneficia de una arquitectura de módulos bien definida, especialmente cuando se despliegan modelos en entornos serverless o contenedores.

En resumen: recuerda que el campo 'type' controla los .js, las extensiones .cjs y .mjs lo sobreescriben, y createRequire es tu puente cuando no puedes convertir un archivo. Con esas tres ideas claras, la migración a ESM deja de ser una pesadilla y se convierte en una evolución natural del ecosistema Node.js.