ES Modules & CommonJS
A real Node.js app is made of many files: one for routes, one for database code, one for helpers and so on. A module is one of those files. It keeps its variables private and shares only what it chooses to export.
Node.js has two module systems. ES modules (ESM) use import and export, the official JavaScript standard that browsers also use. CommonJS (CJS) uses require() and module.exports, and it was Node.js's own system for its first ten years. New code should use ES modules, but millions of existing packages and tutorials use CommonJS, so you need to read both. This lesson teaches ESM first, then CommonJS, then how the two work together.
Choosing ES modules for a project
Node.js decides how to treat each file from its extension and from the nearest package.json file (you will create these with npm in the next lesson). Put "type": "module" in package.json, and every .js file in the project is an ES module:
{
"name": "shop",
"version": "1.0.0",
"type": "module"
}The full rules are:
| File | Treated as |
|---|---|
.mjs | Always an ES module |
.cjs | Always CommonJS |
.js with "type": "module" | ES module |
.js with "type": "commonjs" | CommonJS |
.js with no type field | CommonJS, unless the file contains import or export, then Node.js reruns it as an ES module |
The last row is called syntax detection. It is why a single file with import works even without package.json. Do not rely on it in a project: set "type": "module" so Node.js does not have to guess, and so other tools agree.
Exporting and importing
Create utils/price.js. Each export shares one value; a file can also have one default export:
// utils/price.js
export const GST_RATE = 0.18;
export function withGst(amount) {
return Math.round(amount * (1 + GST_RATE));
}
export default function formatRupees(amount) {
return `₹${amount.toLocaleString("en-IN")}`;
}Now use it from app.js, together with a built-in module and a JSON file:
// app.js
import { readFile } from "node:fs/promises";
import path from "node:path";
import formatRupees, { withGst, GST_RATE } from "./utils/price.js";
import shop from "./shop.json" with { type: "json" };
console.log(`${shop.name}, ${shop.city}`);
console.log(`Bat with ${GST_RATE * 100}% GST: ${formatRupees(withGst(24999))}`);
console.log(path.basename(import.meta.filename)); // this file
console.log(path.basename(import.meta.dirname)); // its folder
const notes = await readFile(path.join(import.meta.dirname, "notes.txt"), "utf8");
console.log(notes.trim());Meera Sports, Jaipur Bat with 18% GST: ₹29,499 app.js shop Open 10 am to 9 pm
Important rules for ES modules in Node.js:
- Write the file extension.
"./utils/price.js"works;"./utils/price"fails withERR_MODULE_NOT_FOUND. - Use the
node:prefix for built-in modules, such asnode:fs/promisesandnode:path. It makes clear the module comes with Node.js, not from npm. - Packages from npm are imported by name, without a path:
import express from "express". - JSON files need the import attribute
with { type: "json" }. import.meta.filenameandimport.meta.dirnamegive the full path of the current file and its folder (added in Node.js 20.11). Use them to build paths to files that sit next to your code, so the program works from any folder.
CommonJS: the older system
Here is the same idea written in CommonJS. You will see this style in older projects, in many npm packages and in most tutorials written before about 2022:
// price.js (CommonJS)
const GST_RATE = 0.18;
function withGst(amount) {
return Math.round(amount * (1 + GST_RATE));
}
module.exports = { GST_RATE, withGst };// app.js (CommonJS)
const path = require("node:path");
const { withGst } = require("./price");
const shop = require("./shop.json");
console.log(`${shop.name}: ${withGst(1000)}`);
console.log(path.basename(__filename));Meera Sports: 1180 app.js
In CommonJS, require() loads a file synchronously and returns whatever it assigned to module.exports. The .js extension can be left out, JSON loads without any attribute, and the variables __filename and __dirname hold the file's path and folder.
| ES modules | CommonJS | |
|---|---|---|
| Load | import x from "./x.js" | const x = require("./x") |
| Share | export / export default | module.exports = ... |
| Standard | Official JavaScript, also in browsers | Node.js only |
| Loading | Asynchronous, top-level await allowed | Synchronous, no top-level await |
| File path | import.meta.filename, import.meta.dirname | __filename, __dirname |
| Extension in path | Required | Optional |
Mixing the two systems
You will often use both at once, because many npm packages are still CommonJS.
ESM importing CommonJS always works. The default import gives you module.exports, and Node.js also tries to offer its properties as named imports:
// in an ES module
import price, { withGst } from "../cjs/price.js";
console.log(price);
console.log(withGst(500));{ GST_RATE: 0.18, withGst: [Function: withGst] }
590CommonJS loading ESM was impossible with require() for years; you had to use the async import() function. That changed with require(esm):
// use-esm.js (CommonJS file)
const greetModule = require("./greet.mjs");
console.log(greetModule.default);
console.log(greetModule.greet("Ananya"));
try {
require("./slow.mjs"); // this module uses top-level await
} catch (err) {
console.log(err.code);
}
import("./slow.mjs").then((mod) => console.log(`Loaded with import(): ${mod.default}`));greet module Namaste, Ananya! ERR_REQUIRE_ASYNC_MODULE Loaded with import(): 42
require() returns the module's namespace object: named exports are properties, and the default export is in .default. It only works for modules without top-level await; for those, require() throws ERR_REQUIRE_ASYNC_MODULE and you must use import().
Common mistakes
ReferenceError: require is not defined in ES module scope. You usedrequire()in an ES module. Useimportinstead.ReferenceError: __dirname is not defined in ES module scope. Useimport.meta.dirnamein ES modules.ERR_MODULE_NOT_FOUNDfor your own file. You left out the.jsextension, or the path is wrong.SyntaxError: Cannot use import statement outside a module. The file is treated as CommonJS, usually becausepackage.jsonsays"type": "commonjs". Set"type": "module"or rename the file to.mjs.- Mixing up default and named exports.
export defaultis imported without braces; named exports need braces with the exact name.
Key points
- Node.js has two module systems: ES modules (
import/export, the standard) and CommonJS (require/module.exports, the original). - Set
"type": "module"inpackage.jsonto make.jsfiles ES modules;.mjsand.cjsforce a system. - In ES modules, write file extensions, use the
node:prefix for built-ins, and import JSONwith { type: "json" }. - Use
import.meta.dirnameandimport.meta.filenameinstead of__dirnameand__filename. - ES modules can import CommonJS packages; CommonJS can
require()ES modules that have no top-levelawait.
Exercise
Create a project folder with "type": "module" in its package.json. Write marks.js that exports a named function grade(marks) (75 or more gives "Distinction", 33 or more gives "Pass", otherwise "Fail") and a default export school set to "Kendriya Vidyalaya, Rohtak". Write main.js that imports both, prints the school, and prints the grade for 81 and for 29.
Show solution
// marks.js
export function grade(marks) {
if (marks >= 75) return "Distinction";
if (marks >= 33) return "Pass";
return "Fail";
}
const school = "Kendriya Vidyalaya, Rohtak";
export default school;// main.js
import school, { grade } from "./marks.js";
console.log(school);
console.log(grade(81));
console.log(grade(29));Kendriya Vidyalaya, Rohtak Distinction Fail
The default import comes first without braces, the named import goes in braces, and the path includes ./ and .js.
Quick Quiz
1. With "type": "module" in package.json, how is a file called old.cjs treated?
The .cjs and .mjs extensions override the type field.
2. Which import works in a Node.js ES module?
ES modules in Node.js need the full relative path including the extension.
3. What should you use instead of __dirname in an ES module?
import.meta.dirname gives the folder of the current module file; process.cwd() is the folder you ran the command from.
4. When does require() of an ES module throw ERR_REQUIRE_ASYNC_MODULE?
require() is synchronous, so it cannot load modules that use top-level await; use import() for those.
Learn Node.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
