27 dic 2025·8 min de lectura

Soluciona la rotura ESM vs CommonJS en aplicaciones Node

Soluciona roturas ESM vs CommonJS detectando rápidamente incompatibilidades de módulo y eligiendo el fix correcto en package.json, build o dependencia.

Soluciona la rotura ESM vs CommonJS en aplicaciones Node

Cómo se manifiesta la rotura ESM vs CommonJS

Node.js tiene dos formas de cargar archivos JavaScript. CommonJS es el estilo más antiguo que usa require() y module.exports. ESM (ECMAScript Modules) es el estilo más nuevo que usa import y export.

La mayoría de las apps no son “puras” de un solo estilo. Tu código puede ser CommonJS, una dependencia puede ser solo ESM, y tu herramienta de desarrollo puede reescribir imports mientras ejecutas en local. Esa mezcla es donde empiezan los problemas.

Cuando la gente dice “incompatibilidad de formato de módulo”, se refiere a que un archivo se está cargando como si fuera CommonJS, pero en realidad es ESM (o al revés). Por ejemplo: tu código hace require('some-lib'), pero some-lib es solo ESM, así que Node se niega a cargarlo con require().

Por eso suele romper justo después de añadir una dependencia o tras desplegar. Muchos setups de desarrollo ocultan la incompatibilidad:

  • TypeScript + ts-node/tsx pueden ejecutar imports estilo ESM en dev incluso si el output compilado acaba siendo CommonJS.
  • Los bundlers pueden hacerlo funcionar localmente al agrupar dependencias, mientras que en producción corre la resolución sin bundle de Node.
  • Un build serverless o Docker puede usar otra versión de Node o un archivo de entrada distinto que tu ejecución local.

Un escenario típico: un prototipo funciona con npm run dev, luego falla en producción justo después de añadir un paquete utilitario pequeño. Localmente, el servidor dev transpila todo al vuelo. En producción ejecutas dist/index.js compilado, Node lo trata como CommonJS, y el primer require() a una dependencia ESM-only lanza la excepción.

Esto afecta a muchos equipos que usan TypeScript, tooling estilo Next/Nuxt y starters generados por IA. El código generado también suele mezclar señales (por ejemplo, "type": "module" en package.json, pero salida CommonJS del build), lo que crea fallos de carga “funciona en mi máquina”. El problema central es simple: el runtime y el output del build no hablan el mismo lenguaje de módulos.

Mensajes de error comunes y lo que suelen indicar

Cuando una app Node mezcla ESM y CommonJS, el mensaje de crash suele ser la pista más rápida. El texto normalmente indica qué pensó Node que era el archivo actual (ESM o CJS) y qué intentó cargar.

ERR_REQUIRE_ESM

Esto ocurre cuando código CommonJS llama a require() sobre un paquete que es solo ESM.

Suele aparecer tras actualizar una dependencia, porque muchas librerías pasaron a ESM en versiones mayores. Desencadenantes comunes incluyen: tu archivo siendo tratado como CommonJS (no hay "type": "module", o el archivo acaba en .cjs), requerir una ruta profunda que evita los puntos de entrada del paquete, o una herramienta (runner de tests, cargador de config) ejecutando tu código en CommonJS aunque tu app sea ESM.

Lo que el error realmente dice: deja de usar require() para esa importación, o elige una versión de la dependencia que todavía soporte CommonJS.

“Cannot use import statement outside a module”

Es la imagen espejo. Node está tratando el archivo actual como CommonJS, pero contiene sintaxis ESM como import.

Causas comunes: falta "type": "module" en package.json, usar .js donde Node espera .mjs, o un paso de build que emite ESM mientras tu runtime lo inicia como CommonJS.

“Named export ... not found” (o sorpresas con el export por defecto)

Suelen venir de asumir que las formas de exportación ESM y CommonJS son iguales. Un módulo CommonJS a menudo exporta un solo objeto, mientras que ESM espera exports nombrados.

Una prueba rápida: si import { thing } from "pkg" falla, pero import pkg from "pkg" funciona, probablemente estás ante un problema de interoperabilidad CommonJS.

“exports is not defined” y sorpresas en tiempo de ejecución

Aparece cuando código que espera globals de CommonJS (exports, require, module) corre en un contexto ESM que no los provee.

Un patrón común es “funcionó en dev”. Un servidor dev transpila todo, pero en producción corren los archivos compilados directamente. El output del build sigue conteniendo exports.foo = ..., y Node lo carga como ESM, así que falla.

Comprobaciones rápidas antes de cambiar nada

Cuando te encuentras con errores de formato de módulo, da la tentación de voltear "type": "module" o empezar a reescribir imports. No lo hagas todavía. Unas comprobaciones rápidas suelen decir si añadiste una dependencia ESM-only, si estás iniciando el punto de entrada equivocado, o si alguna herramienta está ejecutando tu código en un modo inesperado.

Empieza confirmando el runtime exacto. El mismo código puede comportarse distinto bajo node, ts-node, un runner de tests o un bundler. También revisa la versión de Node en el entorno que falla (local, CI, producción). Los defaults y casos límite cambiaron entre versiones de Node, y muchos hosts van por detrás de lo que tienes en local.

Antes de tocar código, confirma:

  • La versión de Node y el comando de inicio en cada entorno (por ejemplo, node server.js vs un runner de TypeScript).
  • El primer archivo que lanza y su extensión: .cjs, .mjs, .js o .ts.
  • El package.json más cercano a ese archivo y si establece "type": "module".
  • El nombre de la dependencia y la ruta de archivo mencionada en la primera línea relevante del stack trace.
  • Si ocurre solo en dev o solo en producción, y qué difiere (output del build, método de instalación, variables de entorno).

Dos patrones rápidos aparecen una y otra vez. Si el stack trace apunta a node_modules/<pkg>/... y dice que no se puede require()-ar, probablemente metiste un paquete ESM-only en código CommonJS. Si apunta a tu output compilado (como dist/index.js), entonces el build y el runtime no concuerdan sobre el formato de módulo.

Un ejemplo pequeño: un prototipo corre localmente vía ts-node (que puede manejar ESM de forma distinta), pero en producción corre plain node dist/server.js. Ese cambio por sí solo puede sacar a la luz la incompatibilidad que debes arreglar.

Paso a paso: diagnostica la incompatibilidad de formato de módulo

La vía más rápida es dejar de adivinar e identificar dónde Node cree que está la frontera entre ESM y CommonJS.

1) Empieza por el primer frame “tu código”

Abre el error y ve al stack trace. Ignora al principio la larga lista de frames dentro de node_modules. Encuentra el primer frame que apunta a un archivo tuyo (ruta del repo) y anota el nombre del archivo y su extensión, la línea que disparó la carga (un import, require o import() dinámico), y el package.json más cercano que controla ese archivo.

Ese frame suele ser donde la decisión errónea de formato de módulo se hace visible.

2) Confirma cómo interpreta Node ese archivo (ESM o CJS)

Node decide ESM vs CJS principalmente por extensiones de archivo y package.json:

  • .mjs se ejecuta como ESM.
  • .cjs se ejecuta como CommonJS.
  • .js depende de type en package.json (si "type": "module", es ESM; de lo contrario es CommonJS).

Una trampa común: crees que estás en CommonJS porque escribiste require(), pero tu paquete es type: module, así que el .js en realidad es ESM.

3) Inspecciona los puntos de entrada de la dependencia

Mira la dependencia que se carga en el punto de fallo. En su node_modules/<pkg>/package.json, comprueba qué está seleccionando Node:

  • main (a menudo CommonJS)
  • module (a menudo ESM, pero Node no siempre lo usa directamente)
  • exports (puede mapear archivos diferentes para import vs require)

Si exports existe, a menudo decide todo. Un paquete puede exportar ESM para import pero no ofrecer ruta CommonJS para require, lo que lleva a ERR_REQUIRE_ESM.

4) Reproduce con el snippet más pequeño posible

Crea un archivo tiny junto a tu app (o en una carpeta de prueba) y prueba solo la importación problemática.

// test-load.js
const pkg = require("the-problem-package");
console.log(pkg);

Luego prueba la versión ESM también:

// test-load.mjs
import pkg from "the-problem-package";
console.log(pkg);

Si una funciona y la otra falla, has confirmado que es una discrepancia de formato (no tu lógica de negocio).

5) Decide qué cambiar: app, build o dependencia

Usa lo que aprendiste para elegir la solución de menor riesgo:

  • Cambia el modo de módulos de tu app (extensiones o type) si controlas la mayor parte del código.
  • Cambia el output de tu build/transpilación si compilas TypeScript o usas bundler.
  • Cambia la dependencia (fija una versión, sustituye el paquete o usa otro punto de entrada) si el paquete ya no soporta tu formato.

Arreglos dirigidos en package.json (type, main, exports)

Make your Node app shippable
Desde imports rotos hasta entry points frágiles, convertimos prototipos en código listo para producción.

Muchas caídas por formato de módulo pueden corregirse sin reescribir todo el código. La meta es dejar claro si tu paquete (o la dependencia) es ESM, CommonJS o ambos.

Empieza por "type". Poner "type": "module" cambia el default de todos los .js en ese paquete a ESM. Eso está bien si te pasas completamente a ESM, pero también puede provocar una cascada de fallos en require(). Si aún tienes archivos CommonJS, considera dejar "type" sin establecer y optar por extensiones archivo a archivo.

Cuando necesites distinto comportamiento por archivo, prefiere extensiones sobre switches globales:

  • Usa .cjs para archivos que deben ser CommonJS (require, module.exports).
  • Usa .mjs para archivos que deben ser ESM (import, export).
  • Usa .js solo cuando el default del paquete (type) coincida con lo que intentas.

Luego revisa tus puntos de entrada. "main" es el entry clásico de Node y suele ser CommonJS. Algunos bundlers miran "module" como entry ESM. Si necesitas ambas builds, apúntalas a archivos distintos (por ejemplo dist/index.cjs vs dist/index.js).

"exports" es lo más potente y lo que más sorprende. Una vez presente, puede bloquear imports profundos como some-lib/dist/internal.js aunque ese archivo exista. Herramientas antiguas y runners de tests también pueden fallar si dependen de rutas profundas. Usa "exports" para exponer solo lo que quieras, pero sé explícito sobre targets import y require cuando soportes ambos.

Si cambias puntos de entrada, evita romper consumidores haciéndolo de forma gradual: mantén "main" estable mientras introduces "exports", exporta tanto objetivos "import" como "require" cuando soportes ambos, y reemplaza rutas profundas por un export público documentado.

Arreglos mediante ajustes de build y transpilación

Muchas fallas ESM/CommonJS no se deben realmente a la dependencia. Vienen del output del build que no coincide con cómo Node ejecuta tu app.

Opciones de TypeScript que deciden qué cargará Node

TypeScript puede compilar código que parece bien en el editor, pero los archivos emitidos pueden no coincidir con tu runtime. Si ejecutas JavaScript compilado, revisa estas opciones primero:

  • compilerOptions.module: CommonJS emite require(...); NodeNext o ESNext emiten import.
  • compilerOptions.moduleResolution: NodeNext entiende reglas ESM (como extensiones y exports).
  • compilerOptions.esModuleInterop y allowSyntheticDefaultImports: pueden hacer que imports compilen aunque la interoperabilidad en runtime siga siendo incorrecta.
  • outDir: asegúrate de que todo el código de runtime provenga de una carpeta (normalmente dist).

Una regla simple: compila al mismo formato de módulo que tu proceso Node espera. Si tu app es ESM, emite ESM. Si tu app es CommonJS, emite CommonJS.

Cuando el bundler “lo arregla” en dev y luego Node falla

Los bundlers y servidores de desarrollo a menudo reescriben o agrupan dependencias, así que la app parece funcionar durante el desarrollo. Luego en producción corre Node sin bundling contra tus archivos compilados, y de repente aparecen errores ESM/CJS.

Para reducir sorpresas, ejecuta localmente el comando de inicio de producción contra el output compilado, no contra el servidor dev.

Evita la mezcla src vs dist

La rotura vuelve cuando tu runtime importa algunos archivos desde src y otros desde dist. Eso mezcla sistemas de módulos y extensiones.

Manténlo limpio:

  • Asegúrate de que en producción se ejecute sólo dist (o solo src si realmente ejecutas TS directamente).
  • Elimina artefactos de build antiguos antes de compilar (archivos obsoletos aún pueden ser importados).
  • Usa rutas de import consistentes que apunten a archivos compilados.

Arreglos ajustando, fijando o cambiando dependencias

Find the real module boundary
Envía tu repo y señalaremos el desajuste ESM vs CommonJS antes de que cambies más código.

A veces la solución más rápida está en la elección de la dependencia, no en tu código. Trata la dependencia como la variable: elige una versión compatible, usa un punto de entrada soportado o cámbiala por una alternativa.

Cambia a una alternativa amigable con CJS (cuando puedas)

Si tu app es CommonJS (usa require) y una dependencia pasó a ESM-only, cambiarla suele ser más limpio que forzar un paso de build solo por ese paquete. Esto es especialmente cierto para utilidades pequeñas.

Al elegir una alternativa, comprueba que soporte el sistema de módulos que usas hoy, que sea compatible con tu versión de Node y que no requiera imports profundos.

Fija una versión compatible (con precaución)

Fijar la versión puede ser la forma más rápida de detener el problema, sobre todo cuando una release cambió el formato de módulo. Trátalo como una medida temporal. Puedes enviar y luego planear la solución real (migrar tu app a ESM o reemplazar la dependencia). También vigila parches de seguridad que puedas perder en versiones antiguas.

Usa el punto de entrada documentado, no un import profundo

Muchas roturas ocurren porque el código importa una ruta interna que antes funcionaba, como some-lib/dist/index.js. Tras una actualización, el paquete añade un exports y bloquea rutas profundas. La solución suele ser importar desde el entry público (o un subpath documentado en exports).

Si la dependencia es ESM-only pero tu app es CJS

Tienes tres opciones realistas: pasar tu app a ESM, reemplazar la dependencia o aislarla.

Aislar suele ser un buen compromiso: carga el paquete ESM en un pequeño módulo wrapper (usando import() dinámico) y mantiene el resto del código en CommonJS mientras planificas una migración más amplia.

Trampas comunes que hacen que la rotura vuelva

Muchos arreglos fallan de nuevo porque la app no es realmente consistente. Funciona con un comando (a menudo dev) y luego falla en tests, CI o producción porque se usa otro entry point o toolchain.

Mezclar require() e import en la misma ruta de ejecución

El problema no es “usar ambos estilos en el repo”. El problema es cuando la misma ruta de ejecución puede correr bajo ambas reglas de módulos.

Ejemplo: parcheas una ruta para usar import() dinámico para una dependencia ESM-only, pero un script CLI o un test aún llega por la ruta antigua con require().

Si debes mezclar formatos temporalmente, deja la frontera clara: un wrapper que haga el import dinámico, y todo lo demás llama a ese wrapper.

Enviar código TypeScript en lugar del JS compilado

Sucede cuando despliegas una carpeta que todavía contiene .ts (o salida con sabor ESM) mientras tu runtime espera CommonJS (o al revés). Localmente parece bien porque ts-node, un servidor dev o un bundler compila por ti.

Una comprobación: mira qué se despliega realmente. Si tu servidor arranca con node dist/index.js, confirma que dist existe y contiene el formato que crees. También confirma que los entry points (main, exports) señalan a archivos compilados, no al código fuente.

Tooling de dev que parchea la carga de módulos

Runners de tests, servidores dev y transpilers pueden enmascarar problemas transformando imports al vuelo. En producción corre Node.js sin transformaciones y aparece la discrepancia sin procesar.

Si dev usa un runner custom pero producción usa node directo, considera que “funciona en dev” no es una prueba hasta que ejecutes el comando de producción localmente.

Añadir "type": "module" para arreglar un archivo y romper todo lo demás

Poner "type": "module" cambia el significado de todos los .js en ese paquete. Puede romper instantáneamente llamadas require(), archivos de configuración que las herramientas esperan en CommonJS y dependencias antiguas que asumen entradas CommonJS.

Si solo necesitas ESM en un área, considera usar .mjs y .cjs, o aislar el cambio en un subpaquete en lugar de voltear todo el proyecto.

Peligros de paquetes duales (comportamiento distinto en ESM vs CJS)

Algunas librerías publican builds ESM y CJS. Node puede elegir una entrada distinta según si usas import o require, y según las condiciones en el exports. Lo complicado es que ambas versiones pueden “funcionar” pero comportarse ligeramente diferente (forma del export por defecto, efectos secundarios).

Cuando la rotura reaparece, fija la versión de la dependencia y bloquea el punto de entrada que quieres (usando el estilo de import documentado). Si la librería es impredecible, cambiar a una dependencia más simple suele ser la solución a largo plazo.

Ejemplo: un prototipo que funciona en dev pero falla en producción

Stop ESM crashes in production
Podemos parchear ERR_REQUIRE_ESM y desplegar una build estable con verificación humana.

Una historia común con prototipos Node: todo parece bien en el portátil, y luego los logs del despliegue explotan. Localmente corriste node server.js, navegaste y la API respondió. En producción, el proceso arranca, llega la primera petición y crashea.

Aquí hay un setup realista. El prototipo tiene un servidor CommonJS (server.js) que usa require() en todas partes. Una dependencia añadida por conveniencia es ESM-only.

El crash suele verse así:

Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
Instead change the require of ... to a dynamic import()

La causa raíz es sencilla: un archivo CommonJS intenta cargar un paquete ESM-only con require(). Node se niega porque ESM y CommonJS tienen reglas de carga distintas.

Dos arreglos suelen funcionar bien, dependiendo de cuánto quieras cambiar.

Opción 1: convertir un archivo (o la frontera) a ESM

Si el archivo del servidor es el único que trae la dependencia ESM-only, mueve esa frontera a ESM.

Puedes renombrar server.js a server.mjs y reemplazar require() por import, o mantener server.js y cargar la dependencia ESM con un import dinámico:

const esmLib = await import('esm-only-lib');

Esto mantiene la mayor parte del código igual, mientras que la dependencia ESM se carga correctamente.

Opción 2: cambiar la dependencia por una alternativa compatible con CommonJS

Si convertir un archivo clave a ESM provoca muchos cambios (tests, configs, otros imports), cambiar la dependencia puede ser más rápido. Elige una dependencia que soporte CommonJS (o tenga builds duales) y actualiza su uso.

Para confirmar el arreglo, no te limites a reiniciar el servidor en el mismo entorno. Haz un rebuild limpio y un inicio en frío: borra outputs de build y caches, reinstala dependencias desde cero, arranca el servidor y llama el endpoint que antes crasheaba.

Próximos pasos si necesitas una solución limpia y lista para producción

Avanza más rápido tomando una decisión y aplicándola de forma consistente: cambia el formato de la app si la mayor parte del código y tooling ya va en esa dirección, cambia la dependencia si es el paquete discordante, o cambia el build si el código fuente está bien pero el output es incorrecto.

Si pides ayuda a alguien más, entrega una instantánea limpia:

  • El texto de error exacto y el stack trace completo
  • Tu package.json (especialmente type, main, exports y dependencias)
  • Tu versión de Node (y si es local, CI o producción)
  • El archivo y la línea que disparan el fallo
  • El comando de ejecución exacto (más cualquier paso de build)

Si esto viene de un prototipo generado por IA que se ha parchado repetidamente, las incompatibilidades de módulos suelen aparecer junto a otros problemas de producción (auth rota, secretos expuestos, estructura difícil de mantener). FixMyMess (fixmymess.ai) se centra en estabilizar esas bases heredadas: diagnosticar qué falla, aplicar los cambios mínimos y verificar el resultado con revisiones humanas. Si quieres un punto de partida de bajo riesgo, su auditoría de código gratuita puede decirte rápidamente cuál es la frontera de módulos equivocada y cuál es la vía de arreglo más segura.

Preguntas Frecuentes

What’s the simplest difference between ESM and CommonJS in Node?

CommonJS usa require() y module.exports, mientras que ESM usa import y export. El problema práctico es que Node carga un archivo como un formato en tiempo de ejecución; si el código o una dependencia espera el otro formato, fallará.

Why does it work in dev but fail after deploy?

Normalmente, tu herramienta de desarrollo está haciendo que la mezcla funcione sin que lo veas. Un servidor de desarrollo, un runner de TypeScript o un bundler pueden reescribir imports o agrupar dependencias, pero en producción suele ejecutarse node contra los archivos de dist, lo que expone la verdadera discrepancia de formatos.

What does `Error [ERR_REQUIRE_ESM]` usually mean?

Casi siempre significa que un fichero CommonJS está llamando a require() sobre un paquete que solo distribuye ESM. Las soluciones rápidas son cambiar esa importación a un import() dinámico envuelto, fijar/sustituir la dependencia por una versión compatible con CJS, o migrar esa parte de la app a ESM.

How do I fix “Cannot use import statement outside a module”?

Node está tratando el archivo como CommonJS, pero el archivo contiene sintaxis ESM. Revisa si falta "type": "module" en package.json, si usas .js cuando deberías usar .mjs, o si el build emite ESM mientras en producción lo inicias como CommonJS.

Why do I get “Named export … not found” or weird default export behavior?

Suele ser una incompatibilidad de interoperabilidad: estás importando exports nombrados de un módulo con forma CommonJS. Intenta importar el módulo entero como default y leer propiedades desde él, o ajusta tu build/runtime para cargar la dependencia en el formato que espera.

What’s the fastest way to find the real source of the mismatch?

Empieza por el primer frame del stack trace que apunta a tu repositorio (no node_modules). Anota la extensión del archivo, la línea exacta que hace import/require y el package.json más cercano que controla ese archivo: eso te dirá si .js se interpreta como ESM o CJS.

How do I tell if a dependency is ESM-only?

Mira el package.json del paquete en node_modules y revisa exports, main y cualquier condición de import/require. Si tiene exports y no ofrece una ruta para require, require() fallará aunque versiones antiguas funcionaran.

Should I just add or remove `"type": "module"` in package.json?

Evita cambiar "type": "module" como primer intento porque altera el significado de todos los .js en el paquete. Sé explícito: usa .mjs para archivos ESM y .cjs para CommonJS, o aísla el cambio en un subpaquete en lugar de voltear todo el proyecto.

What TypeScript/build settings most often cause ESM/CJS breakage?

Haz que la salida del build coincida con cómo arrancas la app. Si en producción ejecutas node dist/server.js, asegúrate de que TypeScript emita el mismo formato de módulo que Node interpretará para ese archivo, y evita mezclas de imports entre src y dist en tiempo de ejecución.

What should I collect before asking for help, and can FixMyMess fix this quickly?

Adjunta el texto exacto del error y el stack trace, la versión de Node (local y producción), el comando de inicio y tu package.json (type, main, exports, dependencias). Si viene de un starter generado por IA y necesitas estabilizarlo rápido, FixMyMess puede ejecutar una auditoría gratuita y aplicar el arreglo más pequeño y seguro, normalmente en 48–72 horas.