What's the difference between CommonJS and ESM modules?
Take a moment.
Say your answer out loud or write it down first — retrieval is what makes it stick. Reveal when you're ready.
Short answer
CommonJS uses require/module.exports with synchronous loading and dynamic requires; ESM uses import/export syntax with static structure parsed ahead of time, enabling tree-shaking, top-level await and async loading. Modern packages declare "type": "module".
Detailed explanation
CommonJS treats modules as runtime objects: require() resolves synchronously anywhere in code (even conditionally), exports are mutable object properties. Its flexibility prevents build-time analysis, so bundlers can't reliably eliminate dead code. ESM's static import/export statements let tools parse dependency graphs before execution: bundlers tree-shake unused exports, circular imports resolve predictably via live bindings (imported values reflect updates), top-level await works, and browsers natively understand it. Interop has sharp edges: CJS default exports become .default under ESM, __dirname/__filename need import.meta.url equivalents, JSON needs assertions/import attributes. Migration reality: many packages ship dual builds (exports map with import/require conditions). New code should standardize on ESM — but knowing how to consume legacy CJS dependencies remains an everyday skill.
Example
// CommonJS
const { readFile } = require('fs/promises');
module.exports = { helper };
// ESM
import { readFile } from 'node:fs/promises';
export function helper() {}
// package.json
{ "type": "module" } // .js files are now ESMInterview tip
Anchor on 'static vs dynamic' — that one word explains tree-shaking, top-level await, and live bindings. Mention dual-package interop pain for real-world credibility.
Common mistakes
- Saying require works in ESM files as-is.
- Forgetting __dirname doesn't exist in ESM.