Durante años, CommonJS (require/module.exports) ha sido la forma por defecto de organizar módulos en Node.js, y la inmensa mayoría de proyectos TypeScript (incluidos los publicados en Analytics Lane) se han construido sobre esa base sin cuestionarla demasiado. Pero el ecosistema lleva tiempo moviéndose en otra dirección, y creo que ha llegado el momento de explicar por qué he decidido migrar mis propios proyectos a ES Modules (ESM), qué gano con ello y, sobre todo, dónde conviene ser cauto antes de hacerlo.
Esta entrada es la primera de una serie de tres. Aquí explico el razonamiento general; en las dos siguientes aplico esta migración a dos proyectos concretos con necesidades muy distintas: una pantalla de API REST con Express (expresslanets publicada en GitHub) y una plantilla de librería TypeScript (tslane publicada en GitHub).
CommonJS vs. ESM, en una frase
CommonJS resuelve los módulos de forma síncrona y dinámica: require() es una llamada a función normal que puede ejecutarse condicionalmente, en cualquier punto del código. ESM, en cambio, usa una sintaxis estática (import/export) que se resuelve antes de ejecutar nada, lo que permite a las herramientas analizar el grafo de dependencias sin ejecutar código. Esa diferencia, aparentemente técnica, es la raíz de casi todas las ventajas que ofrece ESM.

Por qué ahora
Hay tres motivos que, juntos, hacen que 2026 sea un buen momento para dar el paso, cuando quizá hace tres o cuatro años no lo era tanto.
- El ecosistema ya no es neutral respecto a esto. Cada vez hay más paquetes de npm que son ESM-only —
chalk,execa,node-fetch,nanoid, entre muchos otros—. Seguir en CommonJS significa recurrir aimport()dinámico como parche cada vez que quieres usar uno de ellos, lo cual rompe el flujo síncrono habitual y añade fricción real al día a día. - El tooling moderno nace pensando en ESM. Herramientas como Vite, esbuild,
tsxo Vitest (que sustituyen respectivamente a Webpack,tsccomo bundler,ts-nodey Jest en muchos flujos de trabajo actuales) están construidas ESM-first. Cuando las usas sobre un proyecto que sigue en CommonJS, sueles acabar pagando una capa extra de interoperabilidad que simplemente no existe si el proyecto ya es ESM nativo. - Node.js ha dejado clara su dirección. Las funcionalidades nuevas del runtime tienden a aparecer primero, o exclusivamente, en el mundo ESM. Quedarse en CommonJS es, cada año que pasa, quedarse un poco más en el modo “compatibilidad” del ecosistema en lugar de en su vía principal.
Lo que se gana en el día a día
Más allá del argumento de “es hacia donde va todo”, hay beneficios muy concretos:
top-level await: puedes usarawaitdirectamente en el punto de entrada de tu aplicación, sin envolverlo en una función autoejecutable asíncrona. Muy útil para inicializar una conexión a base de datos antes de levantar un servidor, por ejemplo.- Bindings en vivo: los módulos ESM exportan referencias vivas, no copias. Esto hace que las dependencias circulares se comporten de forma más predecible que en CommonJS.
- Mejor tree-shaking: al ser estático, el análisis de qué código se usa realmente es mucho más fiable para los bundlers, lo que se traduce en builds más pequeños para quien consuma tu código.
- Herramientas de desarrollo más rápidas:
tsx, por ejemplo, usa esbuild por debajo en lugar del compilador de TypeScript, así que solo transpila (quita los tipos y ejecuta) en vez de hacer un chequeo de tipos completo en cada arranque. Es notablemente más rápido quets-nodeen el ciclo de desarrollo, y su soporte de ESM es mucho más directo (conts-nodehistóricamente ha sido una fuente constante de flags experimentales y dolores de cabeza).
El matiz importante: aplicación no es lo mismo que librería
Aquí es donde muchas guías genéricas sobre “migrar a ESM” se quedan cortas, porque el cálculo cambia radicalmente según qué estés construyendo.
Si estás migrando una aplicación (un servidor Express, un script, un backend que no publicas en ningún sitio), el único código que tiene que adaptarse a ESM es el tuyo. No hay consumidores externos a los que romper, así que la migración es, en la práctica, una decisión técnica interna: cambias "type": "module", ajustas el tsconfig.json, sustituyes ts-node por tsx, y sigues adelante.
Si estás migrando una librería que otros proyectos consumen (o pueden consumir en el futuro), la historia es bien distinta. Declarar "type": "module" sin más convierte tu paquete en ESM-only: cualquiera que intente const miLib = require('mi-lib') deja de poder hacerlo. Es exactamente lo que le pasó a chalk, execa o node-fetch al hacerse ESM-only, y generó bastante fricción entre quienes seguían en proyectos CommonJS. La alternativa más segura es publicar un paquete dual —CommonJS y ESM a la vez, usando el campo "exports" del package.json con condiciones import/require, de forma que cada consumidor reciba automáticamente la versión que necesita, sin que tengas que elegir entre romper a unos o quedarte a medias con otros.
Lo que no cambia solo: los tests
Si migras a ESM, conviene revisar también cómo ejecutas tus tests, porque Jest y TypeScript+ESM no se llevan especialmente bien: hace falta activar flags experimentales de Node (--experimental-vm-modules), tocar extensionsToTreatAsEsm, y aun así hay casos que fallan de forma poco predecible. Vitest, al estar construido sobre Vite/esbuild igual que tsx, tiene soporte nativo de ESM y TypeScript sin necesidad de configuración adicional, y su API es prácticamente un calco de la de Jest, la migración de los tests en sí suele ser mecánica. Si vas a tocar la configuración del proyecto por el cambio a ESM, es un buen momento para hacer ambos cambios a la vez.
Conclusiones
Migrar a ESM en 2026 tiene sentido para la mayoría de proyectos nuevos, y para bastantes proyectos existentes, pero no es una decisión de “cambiar una línea en el package.json“. Para una aplicación, es relativamente directo. Para una librería, la pregunta correcta no es “¿ESM sí o no?” sino “¿ESM-only o paquete dual?”, y esa respuesta depende de a quién le importa que sigas dando soporte a CommonJS.

En las próximas dos entradas de esta serie aplico este razonamiento a dos casos reales con necesidades opuestas: expresslanets, donde el salto a ESM es directo porque es una aplicación sin consumidores externos, y tslane, donde al ser una plantilla para futuras librerías, la solución pasa por dejar montado un paquete dual desde el principio.
Nota: Las imágenes de este artículo fuero generadas utilizando un modelo de inteligencia artificial.
Deja una respuesta