Modules (CommonJS vs ESM)
Part of the Backend Developer Roadmap
In short
Open any Node project and you'll see require() in some files and import in others. They're two different module systems, and mixing them wrongly causes Node's most confusing errors.
Node's two module systems — the original `require`/`module.exports` (CommonJS) and the newer standard `import`/`export` (ES Modules) — with different loading behavior and syntax.

Think of it like this
Library slip vs pre-booked list
The old way: walk to the counter and fill a slip whenever you need a book. That's require, loaded when the line runs. The new way: hand in your full book list before the day starts, so the library can plan. That's import, known before any code runs.
Watch it happen
Two ways to share code between files
CommonJS
- require() and module.exports
- Loads when the require line runs
- Default for .js without "type"
ES Modules
- import and export
- Imports resolved before code runs
- Used by .mjs or "type": "module"
Start new projects with ESM ("type": "module"). You'll still read CommonJS in older code and many tutorials, so learn to recognise both.
The big ideas
What you need to know
package.json decides
"type": "module" makes .js files ESM. Without it, .js means CommonJS. .mjs and .cjs always force one or the other.
ESM wants extensions
Relative imports need the full file name: import { db } from "./db.js". Node won't guess.
No __dirname in ESM
Use import.meta.dirname and import.meta.filename instead (Node 20.11 and newer).
They can work together
ESM can import CommonJS packages. For the reverse, use dynamic import() or a recent Node version.
See it in code
Walk through the code
1// math.cjs (CommonJS)2// exports.add = (a, b) => a + b;3 4// tax.mjs (ES module)5// export const gst = (amount) => amount * 0.18;6 7// app.mjs8import { add } from "./math.cjs"; // ESM can import CommonJS9import { gst } from "./tax.mjs"; // the extension is required10import { createRequire } from "node:module";11 12const require = createRequire(import.meta.url);13const pkg = require("./package.json"); // old-style require inside ESM14 15console.log(add(2, 3), gst(100), pkg.name);16console.log(import.meta.dirname); // ESM's version of __dirname1/4The same idea in two styles: exports for CommonJS, export for ESM.
Create the three files (uncomment the code in the first two), add a package.json, and run node app.mjs. import.meta.dirname needs Node 20.11 or newer.
Save yourself the pain
Common mistakes
Oops
Writing import { x } from "./utils" without an extension in ESM.
Do this instead
ESM doesn't guess extensions. Write "./utils.js", even in TypeScript projects that compile to ESM.
Oops
Using __dirname in an ES module and getting 'ReferenceError: __dirname is not defined'.
Do this instead
Use import.meta.dirname, or build it from import.meta.url with fileURLToPath on older Node versions.
Your mission
Convert a project to ESM
Quick check
Your package.json has "type": "module". How does Node treat a file named helper.cjs?
Go deeper
- 1Write the same small module in both CommonJS and ESM syntax.
- 2Learn how to enable ESM in a Node project (`"type": "module"` in package.json).
- 3Understand why CommonJS is synchronous and ESM is asynchronous under the hood.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.