Skip to content

Using Modules

In software development, as programs grow larger, code in a single file becomes harder to maintain. To write maintainable code, we group functions into different files, allowing each file to contain less code. In the Node environment, each .js file is referred to as a module.

Benefits of Using Modules

The main advantage of modules is significantly improved maintainability. Additionally, once a module is written, it can be reused elsewhere. We often reference other modules, including built-in Node modules and third-party modules. Using modules also helps avoid naming conflicts; functions and variables with the same name can exist in different modules without interfering with each other.

In the previous section, we created a hello.js file, which is a module named hello (the file name without the .js extension). Let's modify hello.js to create a function that can be called from other files:

javascript
'use strict';

const s = 'Hello';

function greet(name) {
    console.log(s + ', ' + name + '!');
}

module.exports = greet;

The greet() function is defined in the hello module. The last line exports this function, allowing other modules to use it.

Using the hello Module

Now, let's create a main.js file that calls the greet function from the hello module:

javascript
'use strict';

// Import the hello module:
const greet = require('./hello');

let s = 'Michael';

greet(s); // Hello, Michael!

Note that we use Node's require function to import the hello module:

javascript
const greet = require('./hello');

The imported module is stored in the greet variable, which is the same as the greet function exported from hello.js. Thus, main.js successfully references the greet() function and can use it directly.

When using require(), ensure the relative path is correct. Since main.js and hello.js are in the same directory, we use:

javascript
const greet = require('./hello'); // Don't forget the relative path!

If you only write the module name:

javascript
const greet = require('hello');

Node will look for hello.js in built-in modules, global modules, and the current module, which is likely to result in an error:

module.js
    throw err;
          ^
Error: Cannot find module 'hello'
    at Module._resolveFilename
    ...

In this case, check:

  • Whether the module name is correct.
  • Whether the module file exists.
  • Whether the relative path is correct.

CommonJS Specification

This module loading mechanism is known as the CommonJS specification. In this specification, each .js file is a module, and the variables and functions used internally do not conflict. For example, both hello.js and main.js can declare a global variable var s = 'xxx' without affecting each other.

To expose variables (functions are also variables) from a module, use module.exports = variable;. To reference variables exposed by other modules, use var ref = require('module_name');.

Conclusion

To export a variable from a module, use:

javascript
module.exports = variable;

The exported variable can be any object, function, array, etc. To import objects from other modules, use:

javascript
const foo = require('other_module');

The imported object depends on what the module exports.

Understanding Module Principles

If you want to understand the implementation principles of CommonJS modules, continue reading. If not, feel free to skip to the exercises.

When we write JavaScript code, we can declare global variables:

javascript
let s = 'global';

In browsers, extensive use of global variables can lead to conflicts. If a.js uses a global variable s, and b.js also uses s, assigning a new value in b.js can alter the behavior of a.js.

Before the ESM standard, JavaScript did not have a module mechanism to ensure different modules could use the same variable name safely.

So, how does Node.js implement this?

To achieve "modules," no special syntax is needed. Node.js does not introduce any new JavaScript syntax. The magic of module functionality lies in JavaScript's nature as a functional programming language that supports closures. If we wrap a block of JavaScript code in a function, all "global" variables in that block become local variables within the function.

For instance, the code in hello.js looks like this:

javascript
let s = 'Hello';
let name = 'world';

console.log(s + ' ' + name + '!');

When Node.js loads hello.js, it can wrap the code like this:

javascript
(function () {
    // Loaded hello.js code:
    let s = 'Hello';
    let name = 'world';

    console.log(s + ' ' + name + '!');
})();

Thus, the original global variable s now becomes a local variable inside the anonymous function. If Node.js continues loading other modules, their defined "global" variables do not interfere.

By leveraging JavaScript's functional programming features, Node effectively achieves module isolation.

How Does module.exports Work?

The implementation of module.exports is straightforward. Node prepares a module object:

javascript
let module = {
    id: 'hello',
    exports: {}
};

let load = function (module) {
    // Loaded hello.js code:
    function greet(name) {
        console.log('Hello, ' + name + '!');
    }
    
    module.exports = greet;
    // End of hello.js code
    return module.exports;
};

let exported = load(module);
// Save module:
save(module, exported);

Here, module is a variable that Node prepares before loading a JavaScript file, passed into the loading function. We can directly use the module variable in hello.js because it is a function parameter:

javascript
module.exports = greet;

By passing module to load(), hello.js successfully passes a variable to the Node execution environment, where it is saved for future use.

module.exports vs exports

Often, you will see two ways to export variables in a Node module:

Method 1: Assign to module.exports:

javascript
// hello.js
function hello() {
    console.log('Hello, world!');
}

function greet(name) {
    console.log('Hello, ' + name + '!');
}

module.exports = {
    hello: hello,
    greet: greet
};

Method 2: Use exports directly:

javascript
// hello.js
function hello() {
    console.log('Hello, world!');
}

function greet(name) {
    console.log('Hello, ' + name + '!');
}

exports.hello = hello;
exports.greet = greet;

However, you cannot directly assign to exports:

javascript
// This will execute, but the module does not output any variables:
exports = {
    hello: hello,
    greet: greet
};

If you're confused by this distinction, let's analyze Node's loading mechanism:

Node wraps the entire hello.js file in a loading function, where it prepares the module variable:

javascript
let module = {
    id: 'hello',
    exports: {}
};

The load() function ultimately returns module.exports:

javascript
let load = function (exports, module) {
    // Content of hello.js
    ...
    return module.exports;
};

let exported = load(module.exports, module);

By default, the exports variable and module.exports variable are the same and initialized to an empty object {}. Thus, we can write:

javascript
exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };

Or:

javascript
module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };

If we want to export a function or an array, we must assign directly to module.exports:

javascript
module.exports = function () { return 'foo'; };

Assigning to exports is ineffective because module.exports remains an empty object {}.

Conclusion

To export an object, use exports. To export a function or an array, use module.exports.

It is advisable to use module.exports = xxx for exporting module variables to simplify the process.

Exercises

  1. Create hello.js that exports one or more functions.
  2. Create main.js to import the hello module and call its functions.
Using Modules has loaded