Every Node file is one or the other, and mixing them wrongly is the most common source of confusing startup errors.
Node has two module systems:
- ES modules (ESM) — the JavaScript standard:
import/export, top-levelawait, imports are resolved before any code runs. - CommonJS (CJS) — Node's original system:
require()andmodule.exports, loaded synchronously at the momentrequireis called.
New code should be ESM. Node decides the format per file:
.mjsis always ESM,.cjsalways CommonJS..jsfollows the nearestpackage.json:"type": "module"means ESM; no field (or"commonjs") means CJS. Node 22 can also detect ESM syntax in an ambiguous.jsfile, but being explicit is better.
Import built-ins with the node: prefix — import { readFile } from "node:fs/promises" — so they can never be confused with an npm package of the same name.
ESM rules that trip people up:
- Relative imports need the file extension:
import { add } from "./math.js". require,__dirnameand__filenamedon't exist. Useimport.meta.dirnameandimport.meta.filename(Node 20.11+), orimport.meta.url.- JSON needs an import attribute:
import data from "./data.json" with { type: "json" }.
Interop: ESM can import a CommonJS module (its module.exports becomes the default export). Going the other way, Node 22.12+ lets require() load a synchronous ES module (one without top-level await); otherwise use dynamic import(), which works in both systems and returns a promise.
Prefer named exports for most things — they make refactoring and tree-shaking easier and show up in editor autocomplete.
Example
// package.json: { "type": "module" }
// math.js
export const add = (a, b) => a + b;
export default function mean(xs) {
return xs.reduce(add, 0) / xs.length;
}
// main.js
import mean, { add } from "./math.js"; // extension is required
import { readFile } from "node:fs/promises"; // built-in with node: prefix
import pkg from "./package.json" with { type: "json" };
console.log(add(2, 3), mean([1, 2, 3])); // 5 2
console.log(import.meta.dirname); // folder of this file
console.log(pkg.type); // module
const { default: os } = await import("node:os"); // dynamic import, top-level await
console.log(os.platform());Remember
"type": "module"in package.json makes.jsfiles ESM- Use
node:for built-ins and include file extensions in relative imports import.meta.dirnamereplaces__dirnamein ESM- Dynamic
import()works everywhere;require(esm)only for sync ESM
Watch out for
- Writing
import x from './util'without.js— ESM does not guess extensions. - Using
__dirnamein an ES module and getting a ReferenceError.