Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Node's biggest cultural transition: CommonJS (require/module.exports) is what 90% of older code uses; ESM (import/export) is the standard going forward and what every new project should use. Mixing them is a mine field — require() of an ESM package fails synchronously, ESM import of CJS works but with named-export quirks, top-level await is ESM-only. Knowing which you're in (set by "type": "module") and how to interop is essential.
ESM (ECMAScript Modules) and CommonJS are Node.js's two module systems, and they coexist awkwardly: ESM is statically analysable and supports top-level await, while CJS is dynamic and synchronous. The boundary between them is where packages break — you can import a CJS module from ESM, but you cannot require() an ESM module from CJS, and the error message is unhelpful without knowing this rule. Most of the "cannot use import statement in a module" and "ERR_REQUIRE_ESM" errors developers encounter come from implicit assumptions about which system a dependency uses.
"type": "module" in package.json. Use import syntax everywhere. Note that require becomes a hard error.await in an ESM file — works. Try the same in CJS — syntax error. ESM unlocks features.require("some-esm-only-package") (e.g. nano-id v4+). See ERR_REQUIRE_ESM. Now you understand the migration pain.import.meta.url and fileURLToPath to get __filename in ESM (where it doesn't exist).Use these three in order. Each builds on the one before.
In one paragraph, explain the difference between ESM and CommonJS — and why 'pick ESM by default in 2026' is the right rule.
What does `import x from './y.js'` actually do under the hood — synchronous parse, async load, top-level await? And how is that different from `require('./y')`?
I'm migrating a 100-file CJS codebase to ESM. What's the playbook — `"type": "module"`, `.cjs`/`.mjs` mixed, dual packages, migration order?
// ESM (modern, recommended):
// package.json: "type": "module"
// src/lib.mjs OR src/lib.js (with type:module)
export function add(a, b) { return a + b; }
export default { name: "lib" };
// import:
import { add } from "./lib.js";
import lib from "./lib.js";
console.log(add(1, 2)); // 3
// CommonJS (legacy):
// package.json: no "type" or "type": "commonjs"
function add(a, b) { return a + b; }
module.exports = { add };
// require:
const { add } = require("./lib");
console.log(add(1, 2)); // 3
// the boundary:
// in ESM, you can dynamic import() a CJS module:
const cjs = await import("./legacy.cjs");
// but require() of ESM throws ERR_REQUIRE_ESM.
// canonical 2026 setup:
// package.json: "type": "module"
// .mjs for ESM, .cjs for the rare CJS file you need.node main.js