Skip to content
aviral gupta

// I3.3 · ~34 min · Intermediate

ES modules in Node.js: "type", .mjs and .cjs

After this lesson you know why Node.js treats a file as an ES module or as CommonJS, and you replace require, __dirname and JSON loading with their ES module versions.

Lesson 3 of 5 in I3 Modules, Node.js and npm

You will be able to

  • Say how Node.js picks ES module or CommonJS for a file: "type" in package.json, .mjs and .cjs
  • Import Node.js built-ins with node: and CommonJS files from an ES module, without require
  • Find files next to a module with import.meta.dirname, and import JSON with an import attribute
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I3.2. What does main.js print?

    // tools.js
    export default function shout(text) {
      return text.toUpperCase();
    }
    
    // main.js
    import loud from "./tools.js";
    console.log(loud("hi"));
  2. Predict · Activity 2 of 7

    Predict before you read on. package.json says "commonjs". What happens when you run node main.js?

    // package.json
    {"type": "commonjs"}
    
    // main.js
    import {platform} from "node:os";
    console.log(typeof platform());
  3. Practice · Activity 3 of 7

    Fill in the prefix that marks a Node.js built-in module.

    import {readFileSync} from "____:fs";
    from ":fs";
  4. Practice · Activity 4 of 7

    Match each file to how Node.js loads it.

  5. Practice · Activity 5 of 7

    An old habit in an ES module. What happens?

    // main.js
    const fs = require("node:fs");
    console.log(typeof fs.readFileSync);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. data.txt sits next to main.js, and you run node app/main.js from the folder above app. What happens?

    // app/data.txt
    hello
    
    // app/main.js
    import {readFileSync} from "node:fs";
    console.log(readFileSync("data.txt", "utf8").trim());
  7. Apply · Activity 7 of 7

    Mini-task: in a new folder, write package.json with "type": "module", a CommonJS file units.cjs that exports kmToMiles(km) through module.exports, rounded to one decimal, and runs.json with [5, 10, 21.1]. main.js imports both, the JSON with an import attribute, and prints its own file name and the runs in miles. Run node main.js.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

An ES module that uses CommonJS, JSON and a file

main.js is an ES module, because the project’s package.json says "type": "module". It imports built-ins with node:, a CommonJS file whose module.exports arrives as the default export, and a JSON file with an import attribute. It reads notes.txt next to itself through import.meta.dirname, and loads node:os only when it needs it, with a dynamic import.

main.js

import {readFileSync} from "node:fs";
import {join} from "node:path";
import legacy from "./legacy.cjs"; // a CommonJS file: its module.exports
import settings from "./settings.json" with {type: "json"};

console.log(typeof require, typeof __dirname); // CommonJS only
console.log(import.meta.filename.endsWith("main.js"));

// A file next to this module, wherever node is started from:
const notes = readFileSync(join(import.meta.dirname, "notes.txt"), "utf8");
console.log(notes.trim());

console.log(legacy.greet("Ada"));
console.log(settings.theme);

const os = await import("node:os"); // a dynamic import
console.log(typeof os.platform());

legacy.cjs

module.exports = {
  greet(name) {
    return "Hello, " + name;
  }
};

settings.json

{"theme": "dark"}

notes.txt

Remember: import, not require.

Run it with

node main.js

Output

undefined undefined
true
Remember: import, not require.
Hello, Ada
dark
string
  • typeof does not throw for require and __dirname: they are simply not defined in an ES module.
  • legacy.cjs is CommonJS because of its extension, whatever package.json says.
  • join(import.meta.dirname, "notes.txt") finds the file even when node is started from another folder.

Exercises

Exercise 1 of 2

A file next to the module

loadWords() should read words.txt, which sits next to main.js, and return its lines as an array. The starter builds the path with __dirname, which does not exist in an ES module, so the call throws. Use import.meta.dirname instead.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Run the tests: ReferenceError: __dirname is not defined in ES module scope. What replaces it?

  2. Hint 2

    import.meta.dirname is the folder of the current module.

  3. Hint 3

    join(import.meta.dirname, "words.txt")

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import {readFileSync} from "node:fs";
import {join} from "node:path";

export function loadWords() {
  const text = readFileSync(join(import.meta.dirname, "words.txt"), "utf8");
  return text.trim().split("\n");
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

import {readFileSync} from "node:fs";
import {join} from "node:path";

export function loadWords() {
  const text = readFileSync(join(__dirname, "words.txt"), "utf8");
  return text.trim().split("\n");
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadWords} from './main.js';

test('loadWords reads the three words', () => {
  const words = loadWords();
  assert.deepEqual(words, ['apple', 'banana', 'cherry'], `loadWords gave ${JSON.stringify(words)}`);
});

words.txt

apple
banana
cherry

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

A CommonJS helper in an ES module

legacy.cjs is an old CommonJS file with toCelsius and toFahrenheit on module.exports. convert(value, to) in main.js should use them: to "C" converts from Fahrenheit, to "F" from Celsius. The starter calls require inside the function, which an ES module does not have. Import legacy.cjs at the top instead.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Run the tests: require is not defined in ES module scope. How does an ES module load another file?

  2. Hint 2

    A CommonJS file’s module.exports arrives as the default export.

  3. Hint 3

    import temperature from "./legacy.cjs"; at the top of main.js

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import temperature from "./legacy.cjs";

export function convert(value, to) {
  return to === "C" ? temperature.toCelsius(value) : temperature.toFahrenheit(value);
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function convert(value, to) {
  const temperature = require("./legacy.cjs");
  return to === "C" ? temperature.toCelsius(value) : temperature.toFahrenheit(value);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {convert} from './main.js';

test('212 Fahrenheit is 100 Celsius', () => {
  const got = convert(212, 'C');
  assert.equal(got, 100, `convert(212, 'C') gave ${got}`);
});

test('20 Celsius is 68 Fahrenheit', () => {
  const got = convert(20, 'F');
  assert.equal(got, 68, `convert(20, 'F') gave ${got}`);
});

legacy.cjs

module.exports = {
  toCelsius: (f) => Math.round(((f - 32) * 5) / 9),
  toFahrenheit: (c) => Math.round((c * 9) / 5 + 32)
};

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

require in an ES module

const {readFileSync} = require("node:fs");
console.log(readFileSync("main.js", "utf8").length > 0);

What Node.js prints

ReferenceError: require is not defined in ES module scope, you can use import instead

Why, and the fix

A .js file in a package with "type": "module" is an ES module, where require does not exist. Write import {readFileSync} from "node:fs"; at the top. Only if you really need CommonJS, name the file .cjs.

Importing JSON without an import attribute

import settings from "./settings.json";
console.log(settings.theme);

What Node.js prints

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]

Why, and the fix

Node.js only imports JSON when the import says what it expects, so that a file can never run as code by mistake. Add the import attribute: import settings from "./settings.json" with {type: "json"};. Or read the file with readFileSync and JSON.parse.

A named import from a CommonJS file

import {greet} from "./legacy.cjs";
console.log(greet("Ada"));

What Node.js prints

SyntaxError: Named export 'greet' not found. The requested module './legacy.cjs' is a CommonJS module, which may not support all module.exports as named exports.

Why, and the fix

Node.js can only guess named exports from some CommonJS code, and here it cannot. The default export always works, because it is module.exports itself: import legacy from "./legacy.cjs"; then legacy.greet("Ada"), or const {greet} = legacy;.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Which system a file uses

Node.js has two module systems: ES modules (import and export) and the older CommonJS (require and module.exports). For a .js file it looks at the nearest package.json: "type": "module" makes it an ES module, "type": "commonjs" makes it CommonJS. A .mjs file is always an ES module and a .cjs file always CommonJS, whatever package.json says. Without a "type" field, Node.js tries CommonJS and switches when it sees import or export, with a warning. Always write "type": "module" in new projects.

No require in an ES module

require, module.exports, __dirname and __filename belong to CommonJS. In an ES module they are not defined: require("node:fs") throws a ReferenceError. Use import instead: import {readFileSync} from "node:fs", with the node: prefix for built-in modules. An ES module can import a CommonJS file: its module.exports arrives as the default export, import legacy from "./legacy.cjs". To load a module only when needed, use await import("node:os"), which works in both systems.

import.meta and JSON

import.meta.filename is the full path of the current module and import.meta.dirname its folder; import.meta.url is the same as a file: URL. A relative path such as readFileSync("data.txt") is read from the folder where node was started, not from the module’s folder, so build the path: join(import.meta.dirname, "data.txt"). JSON files can be imported with an import attribute: import settings from "./settings.json" with {type: "json"}. Without the attribute, the import fails.

Sources

Last reviewed October 5, 2026