CommonJS vs ES Modules — what are the differences and how do you use each?

3 minintermediatenodejscommonjsesmmodulesimportrequire

Quick Answer

CommonJS (`require`/`module.exports`) is Node's original, synchronous module system that loads modules at runtime. ES Modules (`import`/`export`) are the standardized system: statically analyzable, asynchronous, with live bindings and top-level await. You opt into ESM with `"type": "module"` or an `.mjs` extension.

Detailed Answer

Answer:

CommonJS (CJS) — Node's original module system:

// math.js
function add(a, b) { return a + b; }
module.exports = { add };

// app.js
const { add } = require('./math');
  • Synchronous: require loads and executes the module immediately.
  • Dynamic: you can require conditionally, inside functions, with computed paths.
  • Exports are a copy of the reference at export time.

ES Modules (ESM) — the JavaScript standard:

// math.mjs
export function add(a, b) { return a + b; }

// app.mjs
import { add } from './math.mjs';
  • Static: imports/exports are resolved before execution, enabling tree-shaking and better tooling.
  • Asynchronous loading; supports top-level await.
  • Exports are live bindings — importers see updated values if the exporting module changes them.
  • Import paths generally need the file extension.

How Node picks the system:

  • .cjs → always CommonJS; .mjs → always ESM.
  • .js → depends on the nearest package.json: "type": "module" means ESM, "type": "commonjs" (or absent) means CJS.

Interop:

  • ESM can import CommonJS modules (the module.exports becomes the default export).
  • CommonJS cannot require an ESM module synchronously; use dynamic await import().
  • __dirname/__filename don't exist in ESM — use import.meta.url.

Interview tip: The core distinction is static + async (ESM) vs dynamic + synchronous (CJS). Static structure is what enables tree-shaking.