Stop 02 · Node.js FundamentalsTopics · 5/18

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.

The mascot at a library desk holds a handwritten slip in one hand and a printed list in the other.

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

js
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 __dirname

1/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

~20 min

Quick check

Your package.json has "type": "module". How does Node treat a file named helper.cjs?

Go deeper

  1. 1Write the same small module in both CommonJS and ESM syntax.
  2. 2Learn how to enable ESM in a Node project (`"type": "module"` in package.json).
  3. 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.

Download
InsideEdge