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.

F

Frontend Interview Team

February 08, 2026

~20 min
JavaScript Modules: ESM vs CommonJS (import/export vs require)

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() and module.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); // 1

In 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 import statements
  • build tools can analyze dependency graphs
  • enables tree-shaking
import x from './x.js'; // top-level, static

CJS: 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/export vs require/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?

  • .mjs or package.json type: 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: module rules.

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.