Backend & Node.js · APIs, data and failure
What is the difference between CommonJS and ES modules in Node?
Short answer
CommonJS uses require and module.exports, loads synchronously and caches the exported object. ES modules use import and export, are statically analyzed, support top-level await and use live bindings.
CommonJS was Node's original module system. require runs during evaluation, so it can be conditional and synchronous. ES modules are the language standard, with static imports resolved before evaluation, which enables tree shaking and better tooling.
import { readFile } from "node:fs/promises";
export const config = { retries: 3 };
export async function loadConfig(path) {
const raw = await readFile(path, "utf8");
return JSON.parse(raw);
}Practical differences
- CommonJS exports a value snapshot at require time; ESM bindings stay live
- ESM resolves imports statically, which helps bundlers and circular-dependency analysis
- CommonJS has __dirname and __filename; ESM uses import.meta.url
- ESM requires file extensions in relative specifiers unless a loader resolves them
- Setting type to module in package.json makes .js files ESM
Interoperability is the awkward part. ESM can import a CommonJS default export, but named imports depend on static analysis. CommonJS cannot require an ES module in every Node version, and top-level await makes it impossible synchronously. In modern services I pick one system per package and keep boundaries explicit.
Follow-ups you should expect
Interviewers use these to check whether the first answer was understanding or memorisation.
- 1Why can ESM be tree-shaken more easily than CommonJS?
- 2How do you get __dirname in an ES module?
- 3What is the dual package hazard?
- 4Can CommonJS require an ES module?
Say it in the interview
CommonJS uses require and module.exports and evaluates synchronously with a cached exported value. ES modules use static import and export, support top-level await and keep live bindings. ESM is better for static analysis and bundling, while CommonJS still appears in older packages. I choose one module system per package and handle interop explicitly, because mixing formats can duplicate state or break synchronous loading.
Use this as a model, not a script. Saying it in your own words is what makes it survive follow-up pressure.
Related questions
Reading is step one
Now say it out loud.
Understanding an answer and delivering it under pressure are different skills. Rehearse this topic in a live Backend & Node.js mock interview and get scored feedback on depth, clarity and confidence.