ESM vs. CJS: Navigating JavaScript's Module Divide
JavaScript has two module systems: modern, static ESM (`import`) and legacy, dynamic CJS (`require`). When publishing a library, you must support both. The footgun is shipping only ESM, as it breaks downstream projects that still use `require()`.
WHY IT EXISTS JavaScript originally lacked a built-in module system. Node.js created CommonJS (CJS) with require() for server-side code. Later, the official ECMAScript standard introduced ECMAScript Modules (ESM) with import/export. This created two parallel, incompatible systems that the JavaScript ecosystem now has to bridge.
THE MENTAL MODEL Think of CJS as a dynamic function call: require('./file') runs at execution time and can be placed anywhere, even inside an if-statement. Think of ESM as a static declaration: import X from './file' is hoisted and analyzed before any code runs. This static nature allows for better tooling like tree-shaking (removing unused code), but it's less flexible.
HOW IT WORKS CJS uses require() to import and module.exports to expose functionality. It's synchronous and was the standard in Node.js for years. ESM uses the import and export keywords. It is the official standard for JavaScript and is asynchronous by nature. To enable ESM in a Node.js project, you either name files with .mjs or set "type": "module" in your package.json. The critical incompatibility is that you can import a CJS module into an ESM file, but you cannot require() an ESM module in a CJS file. This one-way gate is the source of most build issues.
WHEN TO USE IT For any public library or package, you should publish both ESM and CJS formats. This is called "dual publishing." It provides maximum compatibility, ensuring your code works for users with modern bundlers like Vite (which prefer ESM) and those with legacy test runners or build systems still reliant on CJS.
WHEN NOT TO USE IT If you control the entire stack for a private application, you can safely choose one format (preferably ESM) and enforce it everywhere. For a simple script or a tool with a very specific runtime target, a single format may be sufficient. For public libraries, however, single-format publishing is a footgun.
ONE CANONICAL EXAMPLE A modern library's package.json solves this with the "exports" field. This field acts as a map, directing different environments to the correct file format. For example, this tells Node.js and bundlers which file to use for an import statement versus a require() call:
"exports": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" }
This setup allows a single package to seamlessly serve both ESM and CJS consumers.
Read the original → nodejs.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.