Module Systems in Node.js
Common JS Modules (Node.js)
The CommonJS module system is one of the most widely used module systems in JavaScript, particularly in Node.js. It is a way to organize and manage code in modular, reusable pieces, which makes it easier to maintain and scale applications. Modules are self-contained and have their own scope. They can expose functionality using module.exports and import functionality using require.
Key Features of CommonJS Modules
-
Synchronous Loading: Modules are loaded synchronously (at runtime) in the order in which they are required. This means that the script waits until the module is fully loaded before moving on to the next step.
-
Exports Object: Each file is treated as a module. Anything you want to expose from a module is added to the module.exports object. Other modules can import this exposed functionality.
-
Single Instance: When a module is loaded, it is executed once and cached. Subsequent calls to require will return the same instance.
-
File-Based Modules: Every .js file is treated as a separate module.
How CommonJS Works
Exporting Code: A module exports its functionality using module.exports. You can export functions, objects, classes, or values.
function greet(name) {
return `Hello, ${name}!`;
}
// Exporting the function
module.exports = greet;Alternatively, you can use the exports shorthand (a reference to module.exports) to export multiple values.
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;Importing Code: To use a module in another file, you use the require function. It takes the path of the module (relative or absolute) and returns the exported functionality. Anything that is exported using module.exports or exports will be an argument to the require function.
const greet = require("./utils");
const math = require("./math");
console.log(greet("Alice")); // Hello, Alice!
console.log(math.add(2, 3)); // 5How Modules are Loaded
When require is called:
-
Node.js resolves the module path.
- If itβs a core module (e.g.,
fs,path), it loads it from Nodeβs core library. - If itβs a file path (e.g.,
./module), Node resolves the path relative to the file requiring it. - If itβs a package (e.g.,
express), Node looks for it in the node_modules directory.
- If itβs a core module (e.g.,
-
Node wraps the code in a function to provide isolation and pass in special variables like
exports,require,module,__filename, and__dirname. -
The module is executed, and its exports object is returned.
Limitations of CommonJS
-
Synchronous Nature: Since
requireis synchronous, it can slow down applications if the required modules are large or involve heavy computations. -
Not Suitable for Browsers: CommonJS is designed for server-side environments like Node.js and doesnβt work in browsers without a bundler like Webpack or Browserify.
-
Modern Alternative: ES Modules (ESM) are the new standard for JavaScript modules, offering a more flexible and dynamic approach to module loading. Node.js has also added support for ESM.
Understanding the module object
In Node.js and JavaScript, the module object is a built-in object that represents the current module. It is part of the CommonJS module system, which is the default module system in Node.js. Every JavaScript file in Node.js is treated as a separate module, and the module object provides information and functionality specific to that module.
console.log(module);Key Features of the module Object
-
Represents the Current Module: The module object provides details about the module file in which it is being accessed.
-
module.exports: The
module.exportsproperty is the object that is exported from a module and made available to other modules when it is required usingrequire(). By default,module.exportsis an empty object . You can assign functions, objects, or values tomodule.exportsto expose them. -
exports Alias: Node.js also provides a shorthand called
exports, which is a reference tomodule.exports. However, you cannot reassignexportsdirectly; you should modify it like an object.
// Correct usage
exports.sayHi = function () {
console.log("Hi!");
};
// Incorrect usage (breaks the reference to `module.exports`)
exports = {
sayHi: function () {
console.log("Hi!");
},
};-
Accessing Module Metadata: The module object contains metadata about the current module. Some important properties include:
module.id: The identifier for the module (usually the absolute path to the file).module.filename: The filename of the module.module.loaded: A boolean flag indicating whether the module has been loaded.module.parent: The module that required the current module.module.children: An array of modules required by the current module.module.paths: An array of paths that Node.js searches when looking for modules.
-
Dependency Management: The
moduleobject is tightly integrated with Node.jsβs module resolution system (require). It keeps track of which modules are loaded and caches them, ensuring that each module is loaded only once. Example of caching:
console.log("Module A is loaded");
module.exports = { value: 42 };const a = require("./a"); // Logs "Module A is loaded"
const aAgain = require("./a"); // Does not log anything, as it's cached
console.log(a === aAgain); // trueModule Wrapper Function
In Node.js, every JavaScript file (module) is wrapped in a special Module Wrapper Function before it is executed. This wrapper function provides a private scope to each module, ensuring that variables, functions, and objects declared in one module do not interfere with others. This mechanism is part of the CommonJS module system in Node.js.
(function (exports, require, module, __filename, __dirname) {
// Module code actually lives here
});This wrapper function is invoked immediately after wrapping, passing in specific arguments like exports, require, module, __filename, and __dirname.
Purpose of the Module Wrapper Function
-
Encapsulation: The wrapper function encapsulates the module code, preventing variables and functions from leaking into the global scope. For example, if you define a variable
const x = 10;infile1.js, it wonβt be accessible infile2.js. -
Provides Useful Variables: The wrapper function provides useful variables like
exports,require,module,__filename, and__dirnameto the module. These variables help in defining and exporting module functionality. -
Supports Module Loading: The wrapper function is essential for loading and executing modules in Node.js. It ensures that each module is executed in its own isolated scope.
exports: An object that is used to export functionality from a module. Initially, it is an empty object{}.require: A function to import functionality from other modules.module: An object that represents the current module.__filename: The absolute path to the current module file.__dirname: The absolute path to the directory containing the current module file.
How the Wrapper Function Works
-
When you write a module like this:
console.log("This is my module"); const greeting = "Hello, world!"; module.exports = greeting; -
Node.js wraps it in a function like this:
(function (exports, require, module, __filename, __dirname) { console.log("This is my module"); const greeting = "Hello, world!"; module.exports = greeting; }); -
Then, Node.js calls the function, passing the appropriate arguments:
(function (exports, require, module, __filename, __dirname) { console.log("This is my module"); const greeting = "Hello, world!"; module.exports = greeting; })(module.exports, require, module, __filename, __dirname);
ES6 Modules (ESM)
The ES Module System (ESM) is the standardized module system introduced in ECMAScript 2015 (ES6) for organizing and sharing JavaScript code across files. It replaces non-standardized solutions like CommonJS (Node.js) and AMD (browsers) with a unified syntax supported natively in modern browsers and Node.js.
- Each JavaScript file is treated as a separate module.
- Code is shared via
exportandimportstatements. - Imports/exports are resolved at parse time (enabling optimizations like tree-shakingβ).
- Modules are loaded asynchronously, allowing for better performance in web applications. (but executed synchronously)
- Modules automatically enforce strict mode (
'use strict'). - Modules have their own scope, meaning variables and functions declared in a module are not accessible in the global scope or other modules unless explicitly exported.
To use ES6 modules in Node.js, you can either:
- Use the
.mjsfile extension for your module files. - Set
"type": "module"in yourpackage.jsonfile, allowing you to use the.jsextension for ES6 modules.
Syntax
- Exporting Code: You can export variables, functions, or classes using the
exportkeyword. There are two types of exports: named exports and default exports.
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
// export as group
const PI = 3.14159;
function add(a, b) {
return a + b;
}
export { PI, add }; // Named exports
export { add as sum }; // Rename while exportingexport default function add(a, b) { return a + b; }
// or
function add(a, b) { return a + b; }
export default add;- Importing Code: You can import code from other modules using the
importstatement. You can import named exports and default exports.
import { PI, add } from "./math.js";
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5// Renaming imports
import { add as sum } from "./math.js";
console.log(sum(2, 3)); // 5// Default import
import add from "./math.js";
console.log(add(2, 3)); // 5import * as math from "./math.js";
console.log(math.PI); // 3.14159
console.log(math.add(2, 3)); // 5import "./math.js"; // No variables are imported, but the module is executedconst modulePath = "./math.js";
import(modulePath).then((math) => {
console.log(math.PI); // 3.14159
});Accessing filename and dirname in ES6 Modules
In Node.js, CommonJS modules traditionally use __filename and __dirname to get the current fileβs path and directory. However, ES Modules (ESM) do not have these variables by default. Instead, you must use the import.meta object and Node.jsβs built-in modules to achieve the same functionality.
Accessing __filename in ESM: Use import.meta.url (which returns the file URL of the current module) and convert it to a file path with the url module.
Accessing __dirname in ESM: Use fileURLToPath to get the file path, then extract the directory with path.dirname.
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename); // Absolute path to the current file
console.log(__dirname); // Absolute path to the directory containing the current fileimport.meta.url is a file URL string like file:///path/to/your/module.js. In browsers: Itβs the URL from which the module was loaded (e.g., https://example.com/module.js)
Difference Between Common.js and ES6 Modules
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Loading | Synchronous (at runtime) | Asynchronous (at parse time) |
| Syntax | require() and module.exports | import and export |
| File Extension | .js (or .cjs) | .js (or .mjs) |
| Scope | Module scope | Block scope |
| Hoisting | No | Yes |
this Context | Global object (in non-strict mode) | undefined (in strict mode) |
__filename and __dirname | Available | Not available (use import.meta) |
| Dynamic Imports | Not supported | Supported (using import()) |
Top-Level await | Not supported | Supported |
| Tree Shaking | Not supported | Supported (with bundlers) |
| Interoperability | Requires esm package for ESM | Can import CJS modules directly |
| Binding | Copy by reference (mutable) | Live bindings (read-only) |
| Circular Dependencies | Handled with caching | Handled with live bindings |
Module Types
-
Native modules: These are built-in modules provided by Node.js, such as
fs,http,path, etc. They are part of the Node.js core library and do not require installation. -
User modules: These are custom modules created by developers. They can be either CommonJS or ES modules, depending on the syntax used.
-
third-party modules: These are modules published on the npm registry. They can be installed using
npmoryarnand can be either CommonJS or ES modules. Examples includeexpress,axios,mongoose, etc.
Understanding package.json
The package.json file is a fundamental part of Node.js projects and JavaScript applications. It serves as the manifest file for your project, containing metadata and configuration information.
// The basic structure of package.json
{
"name": "my-project",
"version": "1.0.0",
"description": "A sample Node.js project",
"main": "index.js",
"scripts": {
"start": "node app.js",
"test": "jest",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"eslint": "^7.32.0"
}
}- name: Your package/project name (required)
- version: The version of your package (required). Follows semantic versioning (semver) format (e.g., β1.0.0β).
- description: A brief description of your package (optional).
- main: The entry point of your application (optional). It specifies the main file to be loaded when your package is required. Default is βindex.jsβ.
- scripts: A set of scripts that can be run using
npm run <script-name>. Common scripts include βstartβ, βtestβ, and βbuildβ. - dependencies: A list of packages that your project depends on. These packages will be installed when you run
npm install. - devDependencies: A list of packages that are only needed for development (e.g., testing frameworks, build tools). These packages are not required in production.
{
"engines": {
"node": ">=12.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/user/repo.git"
},
"bugs": {
"url": "https://github.com/user/repo/issues"
},
"license": "MIT",
"private": true,
"keywords": ["node", "javascript", "example"],
"author": "your name",
"contributors": [
{
"name": "contributor1 name",
"email": "cont.1@mail.com"
},
{
"name": "contributor2 name",
"email": "cont.2@mail.com"
}
],
"homepage": "https://example.com"
}- engines: Specifies the version of Node.js that your package is compatible with (optional).
- repository: Information about the source code repository (optional). It can include the type (e.g., βgitβ) and URL of the repository.
- bugs: Information about where to report issues (optional). It can include a URL or an email address.
- license: The license under which your package is distributed (optional). Common licenses include βMITβ, βApache-2.0β, etc.
- private: A boolean flag that, when set to true, prevents the package from being accidentally published to the npm registry. This is useful for private projects (optional).
- keywords: An array of keywords that describe your package (optional). These keywords help users find your package in the npm registry.
- author: The author of the package (optional). It can be a string or an object with name and email properties.
- contributors: An array of contributors to the package (optional). Each contributor can be represented as a string or an object with name and email properties.
- homepage: The URL of the packageβs homepage (optional). This can be a website or documentation page related to the package.
How package.json is Used
- Dependency Management: Running
npm installreads this file to download all required packages and their dependencies. - Project Metadata: It provides essential information about the project, such as its name, version, and description.
- Scripts: You can define custom scripts to automate tasks (e.g., testing, building) that can be run using
npm run <script-name>. - Publishing: When you publish your package to the npm registry, the
package.jsonfile is included, allowing others to install and use your package easily. - Project Configuration: Tools like ESLint, Babel, Jest read their config from here, allowing you to customize their behavior.