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.
Frontend Interview Team
February 08, 2026
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.