Modules in Node.js

By Syncfusion®, Inc. · Last updated

What a module is, module scope, CommonJS with require and module.exports, the module wrapper, how Node.js finds modules, caching and circular imports, JSON files, ES modules with import and export, import.meta, dynamic import and top-level await, using CommonJS and ES modules together, and running TypeScript files.

Modules in Node.js

Module = one file of code that shares only what it chooses to share.

  • A big program in one file is hard to read.
  • Node.js lets you split it into many small files.
  • Each file is a module.
  • A module exports the things others may use.
  • Another module imports them.

Simple lesson: a module is a file with its own private space and a small public door.

A Real-World Example

Think of the Greenfield School office.

  • The marks clerk keeps the marks register.
  • The fees clerk keeps the fees register.
  • Each clerk works in a separate room.
  • When the principal needs a report, she asks each clerk for a result.
  • Nobody walks into another room and changes the register.

In Node.js, marks.js and fees.js are the two rooms. report.js asks them for results.

You met import and export in Import and Export in the JavaScript course. This lesson shows how Node.js runs them, and the older require style that many Node.js projects still use.

What You Will Learn

In this lesson, you will learn:

  1. What a module is and why it has its own scope
  2. CommonJS: require(), module.exports and exports
  3. The module wrapper, __dirname and require.main
  4. How Node.js finds a module, and the node: prefix
  5. Module caching, circular imports and JSON files
  6. ES modules: import, export, "type", .mjs and .cjs
  7. import.meta, dynamic import() and top-level await
  8. Using CommonJS and ES modules together
  9. Running TypeScript files with Node.js

Module Fundamentals

Module Scope

Module scope = names made in a file stay inside that file.

  • A const in marks.js is not visible in report.js.
  • Two files can both have a variable called total. They never clash.
  • Only exported things can be used by other files.

Three Kinds of Modules

  • Built-in modules come with Node.js, such as node:fs and node:path.
  • File modules are your own files, such as ./marks.js.
  • Package modules are downloaded with npm into node_modules.

Simple lesson: every file is private by default, and it shares only what it exports.

CommonJS: require and module.exports

CommonJS = the first module system of Node.js. It uses require() and module.exports.

  • module.exports = the thing a file gives to others.
  • require("./marks") = load that file and get its module.exports.
  • CommonJS is used when a file ends in .cjs, or when the project has no "type": "module" setting.
  • Many older packages and tutorials still use it.

Click ▶ Run and read the terminal.

  • First line: grades.js ran first, because require loads it before app.js goes on.
  • No .js needed: require("./grades") found grades.js by itself.
  • This lab runs JavaScript only. CommonJS is a JavaScript feature, so the TypeScript menu shows the same files.

module.exports vs exports

  • exports is a short name that points to module.exports at the start.
  • exports.average = ... adds to the same object. That works.
  • exports = { ... } makes exports point somewhere new. Node.js never sees it.
  • Node.js always returns module.exports, not exports.

Never write exports = .... Write module.exports = ..., or add names with exports.name = ....

Simple lesson: use module.exports = ... to share one thing, or exports.name = ... to add names.

The Module Wrapper

Before running a CommonJS file, Node.js wraps it in a function.

(function (exports, require, module, __filename, __dirname) {
  // your file's code goes here
});

Animation: Node.js wraps a file in a function that gives it exports, require, module, __filename and __dirname

grades.js function average(list) ...

function (exports, require, module, __filename, __dirname) the file's own private room

gets tools gives exports

The wrapper gives each file its own scope and five ready names.

  • The wrapper makes module scope. Variables inside a function are private.
  • __filename = the full path of this file.
  • __dirname = the folder this file is in.
  • require.main === module = true only for the file you ran with node.
  • tools/report.js says false, because app.js was the file started with node.
  • Use require.main === module to run code only when a file is started directly.

Simple lesson: the wrapper gives every CommonJS file a private room and five free names.

How Node.js Finds a Module

Specifier = the text inside require("...") or import ... from "...".

  • Relative specifier: starts with ./, ../ or /. It points to your own file.
  • Bare specifier: a plain name like "lodash". It points to a package or a built-in module.
  • node: prefix: "node:fs" always means the built-in module, never a package with the same name.

For a bare name, Node.js looks in node_modules folders, going up one folder at a time.

Animation: require("shout") looks in node_modules of the current folder, then the parent folder, until it finds the package

/project/src/app.js

/project/src/node_modules not here

/project/node_modules/shout found: read package.json "main"

Not found anywhere: Error Cannot find module

Bare names are searched in node_modules, from the current folder upwards.

Files, Folders and main

  • require("./grades") tries grades, then grades.js, then grades.json.
  • require("./utils") on a folder tries utils/package.json and its "main" field.
  • If there is no "main", it tries utils/index.js.
  • These guesses work in CommonJS only. ES modules need the full file name.
  • "main": "lib/shout.js" told Node.js which file is the entry of the package.
  • In a real project, npm install fills node_modules for you. The Packages and npm lesson shows how.

Simple lesson: ./ means your file, a bare name means a package, and node: means built-in.

Module Loading: Cache, Cycles and JSON

Module Caching

Module cache = Node.js runs a module once and keeps its exports.

  • The second require of the same file returns the same object.
  • The file's code does not run again.
  • require.cache holds every loaded module. Deleting an entry forces a reload, which is rarely needed.

Circular Dependencies

Circular dependency = file A needs file B, and file B needs file A.

  • Node.js does not get stuck.
  • The second file gets the unfinished exports of the first file.
  • This often gives undefined values. Move shared code to a third file to fix it.
  • Cache: counter loaded printed once, and both names point to one object.
  • Cycle: b.js saw a.done = false, because a.js had not finished yet.

Loading JSON Files

  • require("./data.json") reads the file and turns it into an object.
  • It is cached like any module.
  • In ES modules, use import data from "./data.json" with { type: "json" }.

Simple lesson: a module runs once, cycles see half-made exports, and JSON loads as an object.

ES Modules in Node.js

ES module (ESM) = the standard JavaScript module system, with import and export.

Node.js treats a file as an ES module when:

  • the file ends in .mjs, or
  • the nearest package.json has "type": "module", or
  • the file has import or export and no setting says otherwise.

A file ending in .cjs is always CommonJS.

TopicCommonJSES modules
Load a moduleconst x = require("./x")import x from "./x.js"
Sharemodule.exports = ...export and export default
File extension in pathsOptionalRequired
Folder and file names__dirname, __filenameimport.meta.dirname, import.meta.filename
LoadsStep by step, while runningAll imports first, then the code
await at the top of a fileNot allowedAllowed

Click ▶ Run, then switch the Language menu and run it again.

  • JavaScript: the import says ./grades.js. The full file name is required.
  • TypeScript: the import says ./grades.ts. Node.js runs .ts files and needs the real extension.
  • import.meta.filename = this file's full path. It replaces __filename in ES modules.

Simple lesson: ES modules use import and export, and always need the full file name.

import.meta, Dynamic import() and Top-Level await

  • import.meta.dirname = the folder of this file.
  • import.meta.filename = the full path of this file.
  • import.meta.url = the same path as a file:// web address.
  • Dynamic import() = load a module later, only when you need it. It gives back a promise.
  • Top-level await = await at the top of an ES module, outside any function.

JSON Import Attributes

  • with { type: "json" } is called an import attribute.
  • Without it, Node.js stops with ERR_IMPORT_ATTRIBUTE_MISSING.
  • It is a safety rule. A file you expect to be data can never run as code.

Simple lesson: import.meta tells a module where it lives, and import() loads code only when needed.

Using CommonJS and ES Modules Together (Advanced)

  • ES module imports CommonJS: works. module.exports becomes the default import.
  • CommonJS requires an ES module: works in recent Node.js versions, if the ES module has no top-level await.
  • createRequire(import.meta.url) = make a require function inside an ES module.

For a new project, pick ES modules and add "type": "module" to package.json. Use CommonJS when you work on an older project that already uses it.

Simple lesson: the two systems can work together, but pick one style for your own new code.

Running TypeScript Files

Recent Node.js versions can run .ts files directly.

  • Node.js strips the types. It removes the type notes and runs the JavaScript that is left.
  • It does not check the types. Use tsc --noEmit for that (see Configuring in the TypeScript course).
  • Imports must use the real extension, such as ./grades.ts.
  • A few TypeScript features need more than stripping, such as enum. They stop with an error.
  • npm install -D @types/node gives your editor the types of process, fs and the other Node.js modules.
  • On older Node.js versions, the tsx package runs .ts files: npx tsx app.ts.
// main.ts: runs with "node main.ts"
type Student = { name: string; marks: number };

const asha: Student = { name: "Asha", marks: 91 };
console.log(`${asha.name} scored ${asha.marks}`);

Output:

Asha scored 91
// main.ts: enum needs more than type stripping
enum Grade { A, B }
console.log(Grade.A);

Output:

/home/asha/school/main.ts:2
// main.ts: enum needs more than type stripping
enum Grade { A, B }
^^^^^^^^^^^^^^^^^^^
console.log(Grade.A);

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode {
  code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'
}

Node.js v22.20.0

Simple lesson: node app.ts runs TypeScript by removing the types, so keep the types simple.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
exports = { ... }It replaces the exportsUse module.exports = { ... }The other file gets an empty object {}
import x from "./grades" in ESMESM guesses .js like requireWrite the full name ./grades.jsERR_MODULE_NOT_FOUND
Using require in an ES moduleBoth work everywhereUse import, or createRequireReferenceError: require is not defined
Using __dirname in an ES moduleIt exists in every fileUse import.meta.dirnameReferenceError: __dirname is not defined
Importing JSON without with { type: "json" }JSON imports like codeAdd the import attributeERR_IMPORT_ATTRIBUTE_MISSING
Two files that require each otherNode.js waits for bothMove shared code to a third fileValues are undefined
enum in a .ts file run by nodeNode.js runs all TypeScriptUse a union type or an objectERR_UNSUPPORTED_TYPESCRIPT_SYNTAX

TL;DR

  1. Module = one file with its own scope. It shares only its exports.
  2. CommonJS = require() and module.exports. Used for .cjs and older projects.
  3. Module wrapper = the function Node.js wraps around a CommonJS file. It gives __dirname and require.
  4. Resolution = how Node.js finds a module.

    • ./x your file, x a package in node_modules, node:x built-in
  5. Cache = a module runs once. Later loads get the same exports.
  6. ES module = import and export. Used for .mjs and "type": "module".
  7. import.meta.dirname = the folder of an ES module.
  8. Dynamic import() = load a module later. It returns a promise.
  9. Type stripping = node app.ts removes types and runs the rest.

Check Yourself

Exercises

What to Remember

  • Every file is a module with its own private scope.
  • CommonJS uses require and module.exports. Never assign to exports directly.
  • ./ is your file, a bare name is a package, node: is built-in.
  • A module runs once. Later loads share the same exports.
  • ES modules use import and export, need full file names and use import.meta.dirname.
  • node app.ts runs TypeScript by stripping types. It does not check them.

Next: Packages and npm. You will install other people's modules and run your project with scripts.