CommonJS vs ES Modules
CommonJS and ES Modules are Node's two systems for splitting code across files: CommonJS uses require and module.exports, while ES Modules use the standard import and export keywords.
Learn CommonJS vs ES Modules in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end of this lesson you'll know how to export and import with both systems, how to pick between .cjs, .mjs, and "type":"module", the difference between default and named exports, how to use top-level await in ESM, and how to dodge the classic interop pitfalls.
What You'll Learn in This Lesson
1️⃣ CommonJS: require & module.exports
CommonJS is the module system Node shipped with for years, so you'll see it everywhere. A file publishes what it wants to share by assigning to module.exports , and another file pulls that in with require('./file') . The call is synchronous — require returns the value right away.
A handy trick: because require returns a plain object, you can destructure the exact pieces you want straight out of it, like {'const = require(\'./math.cjs\')'} .
2️⃣ ES Modules: import & export
ES Modules (ESM) are the official JavaScript standard, the same syntax you use in the browser. You mark shared values with export and load them with import . There are two kinds: named exports (any number per file, imported in {' '} ) and one default export per file (imported with no braces, and you may rename it freely).
ESM unlocks a feature CommonJS never had: top-level await . Because ES Modules load asynchronously, you can write await directly at the top of a file without wrapping it in an async function.
3️⃣ __dirname & __filename in ESM
A common surprise when moving to ES Modules: __dirname and __filename simply don't exist. They were CommonJS globals. In ESM you reconstruct them from import.meta.url using fileURLToPath and dirname .
On recent Node versions there's an even shorter path: import.meta.dirname and import.meta.filename give you the same strings with no imports at all.
Your turn. Fill in the two blanks marked ___ to import the default export and the named export, then run it.
No blanks — just a brief and an outline. Importing a CommonJS file from an ES Module is the single most common interop case in real projects, so it's worth practising. Write it yourself, run it, and check your output against the comments.
📋 Quick Reference — CommonJS vs ES Modules
Practice quiz
Which syntax does CommonJS use to share and load code?
- export and import
- export default and await import
- module.exports and require()
- include and provide
Answer: module.exports and require(). CommonJS publishes values on module.exports and loads them with require(), Node's original module system.
Which keywords do ES Modules use?
- import and export
- require and module.exports
- include and define
- load and share
Answer: import and export. ES Modules use the standard import and export keywords, the same syntax used in browsers.
Is require() synchronous or asynchronous?
- Asynchronous, returning a promise
- It depends on the file size
- It always blocks for one second
- Synchronous, returning the value right away
Answer: Synchronous, returning the value right away. require() runs synchronously and returns module.exports immediately.
How do you import a NAMED export in ESM?
- Without braces: import add from './math.mjs'
- With braces: import { add } from './math.mjs'
- With require()
- With import.meta
Answer: With braces: import { add } from './math.mjs'. Named exports are imported inside braces; the default export is imported with no braces.
How many default exports can a single ES Module have?
- One
- Unlimited
- Zero, defaults are not allowed
- Exactly two
Answer: One. A file can have many named exports but only one default export.
Which feature works in ES Modules but not CommonJS?
- require()
- module.exports
- Top-level await
- synchronous loading
Answer: Top-level await. Because ES Modules load asynchronously, you can use await at the top level without an async wrapper.
What does a .mjs file extension always mean to Node?
- The file is treated as CommonJS
- The file is treated as an ES Module
- The file is minified
- Node ignores the extension
Answer: The file is treated as an ES Module. A .mjs file is always ESM, a .cjs file is always CommonJS, regardless of package.json.
How do you make plain .js files ES Modules by default?
- Rename them to .cjs
- Use require() everywhere
- Add a shebang line
- Add "type": "module" to package.json
Answer: Add "type": "module" to package.json. For .js files Node checks package.json's "type" field; "module" makes them ESM, though the extension always wins.
Why is __dirname undefined in an ES Module?
- Node has a bug
- It is a CommonJS-only global; rebuild it from import.meta.url
- It must be imported from node:path
- It only works on Windows
Answer: It is a CommonJS-only global; rebuild it from import.meta.url. __dirname and __filename do not exist in ESM; recreate them via fileURLToPath(import.meta.url).
When you default-import a CommonJS module from ESM, what do you get?
- Only its named exports
- An empty object
- Its module.exports object
- A promise
Answer: Its module.exports object. Importing a CommonJS module gives you its module.exports as the default import.