What is the difference between CommonJS (require) and ES modules (import) in Node.js?
CommonJS uses require and module.exports, loads files synchronously, and was Node's original system. ES modules use import and export, are the JavaScript standard, are analysed before running, and support top-level await. Node supports both.
Explain it simply
Both are ways to split code into files and share functions between them. Think of two ways of ordering food in a hostel. CommonJS is like walking to the mess and picking up your plate when you need it: require runs at that moment and hands you whatever the other file exported. ES modules are like a pre-booked thali: before anything runs, Node reads every import line, plans which files are needed, links them, and only then starts executing.
CommonJS came first because JavaScript had no module system when Node was created. ES modules (ESM) were added to the language later, in ES2015, and are what browsers use too. New projects usually choose ESM.
Example
CommonJS:
// math.cjs
function add(a, b) { return a + b; }
module.exports = { add };
// app.cjs
const { add } = require("./math.cjs");
console.log(add(2, 3)); // 5ES modules:
// math.mjs
export function add(a, b) { return a + b; }
// app.mjs
import { add } from "./math.mjs";
console.log(add(2, 3)); // 5How it works
Node decides which system a file uses:
.cjsis always CommonJS,.mjsis always ESM.- For
.js, it checks the nearestpackage.json."type": "module"means ESM; otherwise CommonJS.
Key differences:
| CommonJS | ES modules | |
|---|---|---|
| Syntax | require, module.exports | import, export |
| Loading | Synchronous, at runtime | Parsed and linked before running |
Top-level await | No | Yes |
__dirname, __filename | Available | Use import.meta.dirname / import.meta.filename |
| File extension in relative paths | Optional | Required (./math.js) |
| Exports | A copy of the value at the time of export | Live bindings |
Real-world use
Older Express apps and many tutorials use CommonJS. Modern packages, Vite projects and newer Node services use ESM. Because ESM is static, bundlers can do tree shaking and remove unused exports, which keeps frontend bundles smaller.
Common mistakes
- Forgetting the file extension in an ESM import (
./utilsinstead of./utils.js). - Using
__dirnameinside an ES module and getting aReferenceError. - Mixing
requireandimportin the same file without knowing which mode the file is in. - Expecting
importstatements to work conditionally inside anif. Use dynamicimport()for that.
What to say in the interview
"CommonJS is Node's original module system, with require and module.exports. It loads modules synchronously when the line runs. ES modules are the official JavaScript standard, with import and export. They are statically analysed before execution, which enables tree shaking and top-level await, and their exports are live bindings. Node picks the system by file extension or the type field in package.json. For new code I use ES modules, and I use dynamic import() when I need to load something conditionally."
Follow-up questions
- Can a CommonJS file load an ES module?
- What does
"type": "module"in package.json change? - What is tree shaking and why does it need ES modules?
Go deeper
Read these once the basics above feel easy.
An ES module can import a CommonJS module; the module.exports object becomes the default export. Going the other way used to require dynamic import(), which returns a promise. Recent Node versions also allow require() of an ES module as long as that module graph has no top-level await. If you publish a library, the exports field in package.json lets you ship separate entry points for import and require users.