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.
Interview question
For a public JavaScript library, what is the most critical reason to dual-publish both ESM and CJS formats?
- a.To provide flexibility for developers who prefer dynamic module resolution over static declarations.
- b.To ensure that projects using require() can successfully import the library's modules.Correct
- c.To allow consuming applications to choose between synchronous and asynchronous module loading.
- d.To enable advanced tooling features like tree-shaking for all consumers, regardless of their module system.
Why? this is the answer
The card states that dual publishing provides maximum compatibility, ensuring code works for users with legacy systems reliant on CJS, as CJS cannot require() an ESM module. While ESM enables tree-shaking, dual publishing is not primarily for extending tree-shaking benefits to CJS consumers; it's for CJS compatibility itself.
Just read this? Test yourself on what you have been reading.
Read the original → nodejs.org
Put your scrolling time to good use
Learn one idea, try a quiz and save useful cards for revision. Tezvyn makes it easy to learn and stay current in your tech field, a few minutes at a time.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on javascript — each one lists the topics its interview covers.
See open roles