Modules: CommonJS vs ESM
A module is a file that exports values other files can import. Node has two module systems: the older CommonJS and the modern ES Modules (ESM).
CommonJS (the classic way)
CommonJS uses require and module.exports:
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// app.js
const { add } = require("./math.js");
console.log(add(2, 3));
Node treats .js as CommonJS unless you opt into ESM.
ES Modules (the modern way)
ESM uses import and export — the same syntax as the browser:
// math.mjs
export function add(a, b) {
return a + b;
}
// app.mjs
import { add } from "./math.mjs";
console.log(add(2, 3));
Enable ESM for a whole package by setting the type in package.json:
{ "type": "module" }
Then any .js file uses import/export.
Named vs Default Exports
export const name = "Ada"; // named
export default function greet() {} // default
import greet, { name } from "./lib.js";
Prefer named exports — they rename safely and are easier to discover.
Built-in Modules
Node ships modules you import with the node: prefix:
import { readFile } from "node:fs/promises";
import path from "node:path";
import os from "node:os";
Importing npm Packages
Bare names resolve from node_modules:
import express from "express";
Relative paths need ./ or ../. Without them, Node looks in node_modules.
Best Practices
- Prefer ESM — It is the standard and works in browsers too.
- Use
node:for built-ins — Clearer and collision-proof. - One module, one job — Keep files focused.
- Use named exports — Avoid default exports in shared libraries.
Common Mistakes
- Forgetting
./—import "./math.js", notimport "math.js". - Mixing systems — ESM cannot
require; CommonJS needs dynamicimport()for ESM. - Omitting the extension in ESM — Node requires
./math.js, not./math. - Circular imports — Modules that import each other can load as
undefined.