ES Modules & CommonJS

Node.js · Lesson 5 of 30 7 min read Updated

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:

JSON
{
  "name": "shop",
  "version": "1.0.0",
  "type": "module"
}

The full rules are:

FileTreated as
.mjsAlways an ES module
.cjsAlways CommonJS
.js with "type": "module"ES module
.js with "type": "commonjs"CommonJS
.js with no type fieldCommonJS, 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:

JavaScript
// 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:

JavaScript
// 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());
Output
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 with ERR_MODULE_NOT_FOUND.
  • Use the node: prefix for built-in modules, such as node:fs/promises and node: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.filename and import.meta.dirname give 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:

JavaScript
// price.js (CommonJS)
const GST_RATE = 0.18;

function withGst(amount) {
  return Math.round(amount * (1 + GST_RATE));
}

module.exports = { GST_RATE, withGst };
JavaScript
// 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));
Output
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 modulesCommonJS
Loadimport x from "./x.js"const x = require("./x")
Shareexport / export defaultmodule.exports = ...
StandardOfficial JavaScript, also in browsersNode.js only
LoadingAsynchronous, top-level await allowedSynchronous, no top-level await
File pathimport.meta.filename, import.meta.dirname__filename, __dirname
Extension in pathRequiredOptional

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:

JavaScript
// in an ES module
import price, { withGst } from "../cjs/price.js";
console.log(price);
console.log(withGst(500));
Output
{ GST_RATE: 0.18, withGst: [Function: withGst] }
590

CommonJS loading ESM was impossible with require() for years; you had to use the async import() function. That changed with require(esm):

JavaScript
// 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}`));
Output
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 used require() in an ES module. Use import instead.
  • ReferenceError: __dirname is not defined in ES module scope. Use import.meta.dirname in ES modules.
  • ERR_MODULE_NOT_FOUND for your own file. You left out the .js extension, or the path is wrong.
  • SyntaxError: Cannot use import statement outside a module. The file is treated as CommonJS, usually because package.json says "type": "commonjs". Set "type": "module" or rename the file to .mjs.
  • Mixing up default and named exports. export default is 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" in package.json to make .js files ES modules; .mjs and .cjs force a system.
  • In ES modules, write file extensions, use the node: prefix for built-ins, and import JSON with { type: "json" }.
  • Use import.meta.dirname and import.meta.filename instead of __dirname and __filename.
  • ES modules can import CommonJS packages; CommonJS can require() ES modules that have no top-level await.

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
JavaScript
// 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;
JavaScript
// main.js
import school, { grade } from "./marks.js";

console.log(school);
console.log(grade(81));
console.log(grade(29));
Output
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?

2. Which import works in a Node.js ES module?

3. What should you use instead of __dirname in an ES module?

4. When does require() of an ES module throw ERR_REQUIRE_ASYNC_MODULE?

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