Modules in Node.js

Uriel Rodriguez
5 min readMar 7, 2021

It can be said that the building blocks of an application are modules. Modules are isolated pieces of code that when put together construct the larger functioning application. Node.js accomplishes this abstraction through the CommonJS standard, which provides a system of using files to encapsulate code and make code available to other files. Through a module system, files can be imported, exported, and plugged into other files to carry out some function in constructing the overall application. Modules also help solve the old issues JavaScript had with code existing within a global scope. Let’s take a look at how this is done with CommonJS.

The main idea of modules is to break down large pieces of code into smaller pieces. So by storing relevant code in individual files, code can be plugged into other portions of an application via importing. Importing a file means loading in code that is made available for use within the file. A simple import statement with CommonJS would be something like the following.

const sample = require('./sample.js');// if sample.js provided a function
sample();
// if sample.js provided a variable
console.log(sample);
// etc

Very simply expressed, the first step in utilizing modules is importing the code within a file, in this case, “./sample.js” and storing it in a variable. Afterward, depending on what the imported code is, whether a function, variable, object, or anything else, it is used like any other piece of code. In CommonJS, importing a file involves using the require() function, which takes in a module identifier, and loads in that module within the current module. Imported code can have three different origins: core modules which are the modules natively provided by Node.js, modules within a project directory, and third-party packages, or installed modules. In this example, a file from within the project directory is imported, which can be identified by the relative path module identifier that is passed into the require() function, “./sample.js”. When importing the built-in or third-party modules, the process is the same, a single word module identifier is passed into require().

// built-in module used for creating HTTP servers
const http = require('http');
// third-party package used for making HTTP requests
const axios = require('axios');

The other side of this process is the exportation of code so that other modules can access it within their scope. Node.js makes that possible by providing files with a “module” object which represents the file. Within the module object is an “exports” property that serves to capture the code that is to be made available to other files or modules.

// sample.jsconst sample = 'sample variable string';
const sample2 = 'sample2 variable string';
module.exports = sample;
// importing module
const sample = require('./sample.js');
console.log(sample); // => sample variable string
console.log(sample2); // => Reference error

Expanding on the example above, here we see the structure within “sample.js” that allows other modules to import its API for use. Any code that is defined within a module is private to other modules unless it is explicitly exported. When I tried to log the “sample2” variable, a reference error was thrown because it was not explicitly exported via assignment to the “exports” property on the module object. This is how Node.js allows files access to encapsulated code. Node.js also provides the same exporting functionality via the shorthand “exports” object.

// sample.jsconst sample = 'sample variable string';exports.sample = sample;

Just as in the first example where the “sample” variable was assigned to module.exports, in this example, the equivalence when working solely with exports is to assign a property on exports. If exports is redefined by assigning it to some code, then the module.exports functionality will be overwritten and will not work.

exports = sample; // will cause failure

As of Node.js version 14.x, the standard JavaScript module system, ES modules, has also be adopted and provides alternatives to utilizing modules along with some key differences. The ES module system utilizes the “import” statement to import modules and does so asynchronously as opposed to the synchronous require() function. For example:

// importing with ES Modulesimport sample from './sample.mjs';

Unlike CommonJS, using ES modules does not require explicitly defining a variable to store the exported code, and takes in a URL as the module identifier. This process is carried out for core modules and third-party packages. When exporting code from a module, it is also handled differently than with CommonJS, for example:

// sample.mjsexport const sample = 'a sample';
export default const sample2 = 'a second sample';

Here we are exporting both variables from sample.js in two different manners, the first being a named export and the second being a default export. Named exports are identified by a single “export” statement prefixed to some code, and can be done many times within a single module. The importing module accesses the exports via the name of the variables, hence named exports.

// sample.mjsexport const sample = 'a sample';
export const sample2 = 'a second sample';
// importing sample.jsimport { sample, sample2 } from './sample.mjs';

The second kind of export is the default export illustrated above by prefixing “export default” to some code. This kind of export can only be carried out once per module and is generally reserved for modules that export a single piece of code, such as a class or function.

Aside from the syntactical differences, CommonJS and ES modules provide other key differences. Like the “module” object which is injected by default into each CommonJS module by Node, other values are made available within the modules. The __filename and __dirname are variables that are made available within every CommonJS module, useful for various configurations.

// within a CommonJS module
// by default .js extension
console.log(__filename, __dirname); // double underscore

When working with ES modules, these variables are not available and need to be computed. This is carried out by the only value that is injected within ES modules, the “import.meta” property which helps identify the URL which maps to the current module.

// sample.mjs
// .mjs extension is used for ES modules
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename, __dirname);

As mentioned before the ES module takes a URL as the module identifier so the injected import.meta provides access to a “url” property that computes the module’s URL. From the computed URL, the file name can be determined, and subsequently, the directory name as well. Another thing to take note of when using ES modules is the file extension, “.mjs”. The default module system for Node.js is CommonJS which defaults to file extension “.js”. This default module system can be replaced via the package.json file by adding a “type” field and specifying “module” for ES modules, or “commonjs”.

{
"type": "module" or "commonjs"
}

Since Node.js’ inception, JavaScript has progressed and adopted new functionality which Node.js continues to incorporate into its ecosystem. As a result, Node.js provides various ways to utilize modules, include CommonJS and ES modules. Understanding the differences between these module systems is important in knowing when one module is ideal over the other. To learn more Node.js and its module system, check out the link below for great information.

--

--