JavaScript Modules: ESM vs CommonJS (import/export vs require)
A deep, interview-ready guide to JavaScript modules: ESM vs CommonJS, named vs default exports, live bindings, top-level await, Node/browser differences, and common pitfalls.
On this page
Modules are a classic interview topic because they sit at the intersection of:
- language features (exports/imports)
- runtime behavior (Node vs browser)
- build tools (bundlers, tree-shaking)
This post gives you a clean mental model and practical examples.
30‑second interview answer
ESM (import/export) is the modern JavaScript module system with static imports (enables tree-shaking), support for top-level await, and live bindings. CommonJS (require, module.exports) is the older Node.js system with dynamic loading and different interop rules. In interviews: prefer ESM for new code, but understand interop and runtime differences.
Key points
- ESM imports are static; CJS is runtime/dynamic.
- ESM has live bindings; CJS exports are values.
- Node supports both, but interop has gotchas.
- Bundlers love ESM (tree-shaking).
1) What problem do modules solve?
Before modules, JS files leaked into the global scope and dependency order was painful.
Modules give you:
- explicit imports/exports
- isolated scope by default
- reliable dependency graphs
2) Two module systems you’ll see
A) CommonJS (CJS)
- historically Node.js default
- uses
require()andmodule.exports
// math.cjs
function add(a, b) { return a + b; }
module.exports = { add };
// app.cjs
const { add } = require('./math.cjs');
console.log(add(1, 2));B) ECMAScript Modules (ESM)
- modern JavaScript standard
- uses
import/export
// math.mjs
export function add(a, b) { return a + b; }
// app.mjs
import { add } from './math.mjs';
console.log(add(1, 2));3) Named exports vs default exports
Named exports
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
// import
import { PI, area } from './circle.js';Pros:
- refactor-friendly
- clearer API
- better tooling support
Default export
export default function area(r) { return Math.PI * r * r; }
import area from './circle.js';Pros:
- convenient for “one main thing” modules
Interview tip:
- prefer named exports for libraries
- default export is fine for single-purpose modules
4) The big conceptual difference: how values are bound
ESM has live bindings
This is the most important interview detail.
If a module exports a variable, imports see updates (because it’s a live binding).
// counter.js
export let count = 0;
export function inc() { count++; }
// app.js
import { count, inc } from './counter.js';
console.log(count); // 0
inc();
console.log(count); // 1In ESM, this works as expected.
CJS exports are more like a snapshot (object)
CJS exports are properties on module.exports.
You can mutate the exported object, but semantics differ.
5) Loading and execution order
ESM: static analysis (mostly)
ESM imports are (mostly) static:
- you can’t conditionally import with normal
importstatements - build tools can analyze dependency graphs
- enables tree-shaking
import x from './x.js'; // top-level, staticCJS: dynamic
CJS require() is just a function call.
if (flag) {
const x = require('./x');
}This can be useful but makes static analysis harder.
6) Node.js specifics (important in interviews)
How Node decides ESM vs CJS
Node uses:
- file extension:
.mjs= ESM,.cjs= CJS - or
package.json:
{
"type": "module"
}If type is module, then .js files are treated as ESM.
If not, .js defaults to CJS.
7) Interop (mixing ESM and CJS)
This comes up a lot in real apps.
Importing CJS from ESM
import pkg from 'some-cjs-package';Often, pkg is the CJS export object.
Requiring ESM from CJS
This is trickier. In Node, you generally need dynamic import:
(async () => {
const mod = await import('./esm-module.js');
})();Interview answer:
- CJS → ESM requires async
import()or tool support.
8) Top-level await (ESM feature)
ESM supports top-level await in modern runtimes:
const res = await fetch(url);
export const data = await res.json();This is not available in CJS the same way.
9) Bundlers, tree-shaking, and why ESM wins for frontend
Frontend bundlers (Vite/Webpack/Rollup) can tree-shake ESM better because imports are static.
Tree-shaking benefit:
- smaller bundles
- faster load
If you build libraries for the web, ESM is usually the better format.
10) Interview Q&A
Q: ESM vs CJS — main differences?
- Syntax:
import/exportvsrequire/module.exports - ESM is static + supports live bindings + top-level await
- CJS is dynamic and synchronous
Q: Why is ESM better for tree-shaking?
- Static imports allow bundlers to see what’s used.
Q: What does “live binding” mean?
- Imports reflect updates to exported variables.
Q: How do you use ESM in Node?
.mjsorpackage.jsontype: module.
Summary checklist
- I can explain ESM vs CJS in one minute.
- I know why ESM enables tree-shaking.
- I can explain live bindings.
- I know Node’s
.mjs/.cjs+type: modulerules.
Summary
- Know the syntax differences.
- Know the live binding vs object export mental model.
- Know Node’s rules (
.mjs/.cjs,type: module). - For frontend + bundlers, ESM is the modern default.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
Written by Rajesh Dhiman
Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.