Understanding how modularity works and the things we can use

Photo by Markus Spiske on Unsplash

In Node, the modularity is a first-class concept. In the Node.js module system, each file is treated as a separate module.

So, if you are creating, let’s say, a demo.js file, this implies you are creating a module in Node. Basically modules help us encapsulating our code into manageable chunks.

Anything that we define in our module (i.e. in our JavaScript file) remains limited to that module only, unless we want to expose it to other parts of our code.

So, anything we define inside our module remains private to that module only.

How To Create a Module

Creating a module in Node is very simple, just create a file and you are good to go.

That’s it, now you have a module in Node, nothing fancy, just simple creation of a file.

Types of Module

  • Core module: Modules that come shipped with Node.js, e.g. https, os, fs, net, etc.
  • Third-party module: Modules that you install from any package manager. We use these modules to accomplish or simplify any existing task. For example, to simplify our web API development we use express, or to deal with date and time we use moment.
  • Local module: These are the modules that we create for our own use. These modules basically consist of core business logic of our code.

How Modularity Worked Before

Prior to modules in Node.js or ES5 modules, the modularity in JavaScript was achieved using IIFE (Immediately Invoked Function Expression), which is, as the name suggests, a function which is invoked immediately after it is defined.

Now, if we run this code, we will get the output as 5.

The function sum is defined inside this IIFE and if any code outside that IIFE tries to access the sum function, it will result in ReferenceError: sum is not defined, i.e. the sum function is private to this particular IIFE.

So, how do we access this sum function outside of this IIFE?

To expose our sum function outside IIFE, we create an object (exportObj) outside IIFE, then, through closure, we access that object inside our IIFE and assign our sum function to one of its property.

After that, we call the sum function on the exportObj object outside the IIFE. This time, we are able to get result without any errors.

How Modularity Works in Node.js

We have seen above that, to achieve modularity prior to Node and ES5, we used functions.

In Node.js, this is the same way in which we achieve modularity, i.e. using functions only, but there is one gotcha — this wrapping function that wraps our code is not written by us but is automatically added by Node for us.

Let’s look at an example to understand it better.

Let’s say we defined one file, named sum.js, with the following content:

The code above, in sum.js, is encapsulated by Node.js into a function.

So, in Node.js, this code is wrapped and looks something like this in our running environment:

Everything is wrapped as we wrapped in our IIFE but, here, this wrapper function gets some arguments. We will discuss them in detail later in this post.

To check whether your code is wrapped in a function and whether we are receiving these arguments, or not. In JavaScript, we know that all functions receive an argument called arguments, so, if we get arguments in our code, it confirms that our code is inside a function.

Output of Code 6 (pic 1)

We can see that we get the output of arguments (arguments is an array-like object, whose keys are numeric, which is passed to every function by default). So, it confirms that our code is wrapped inside a function and that function receives five arguments, which are given by Node.js.

Let’s discuss these five arguments one-by-one.

Exports

This is an object used to expose our functionalities in one module, so these functionalities can be used in other modules.

We can expose anything, this can be a function, variable, constants, classes, etc. As we have done above in the How modularity worked before section, we have created a property on exportObj and then assigned a value to it.

The same way we do it with exports object — we create a property on the exports object and then assign a value, or whatever you want to expose (variable, function, classes, constants), to that property.

Here, we expose the multiply function by assigning the function reference to a newly created multiply property on the exports object, i.e. multiply function is only available outside this module, not the sum function.

Note: Do not provide a new reference to this exports object, i.e. don’t assign a new object to the exports argument. (We will discuss why not to do this.)

Require

This is a function that we use to import or require the functionalities from other modules. It is a compliment to the exports object, which is used to export functionalities. require, on the other hand, is used to import those functionalities.

To require a module, we call the require function with either the path of the module (absolute or relative), which starts with /, ./, or ../ in the case of local modules, or the name of the module in the case of core modules and third-party modules.

Then, it returns the exported content of the module that we require.

Note: Basically, we get the reference of the object module.exports (we will discuss this) when we require a module.

In Code 7, we implemented two functions, sum and multiply, but we have exported only multiply, so only that one is available outside of the operations.js module. That is why we will get an error if we try to call sum.

Node’s require function has a lot more to offer than just importing the functionalities, we will dive deeper into this.

Module

This is the third argument passed, the module variable is a reference to the object representing the current module. It has various useful properties which we can see in the terminal with console.log(module) in any module.

Let’s say we do console.log(module) in app.js (Code 9) and operations.js(Code 7). We get the following output:

console.log(module) in operations.js (pic 2)
console.log(module) in app.js (pic 3)

The module object contains all the data regarding our module, such as: “Who is its parent? Who are its children? What are all the paths it took to resolve third-party modules? Is it completely loaded, or not?”

But the most important property of the module object is the exports property, we can also use this exports property on the module to export our data, rather than using exports arguments of the wrapper function.

So, this is the second way of exporting functionalities out of our module.

Note: We will see the difference between exports and module.exports, and how they are connected to each other.

Summary of the module object

  1. module.filename is the fully resolved filename of the module.
  2. module.id is the identifier for the module. Typically, this is the fully resolved filename, except for the main module, it is ‘.’ (period), see pic 3. Main module is the module that spins up your Node application, e.g if we write node app.js in the terminal, then app.js is the main module.
  3. module.path is the directory name of your name module.
  4. module.parent is an object which refers to the parent module.
  5. module.children is an array of all the children module objects.
  6. module.loaded is a boolean property which tells us whether or not the module is done loading, or is in the process of loading.
  7. module.paths is an array of all the paths that Node will look up to resolve a module.

Some of you might have noticed in pic 2 and pic 3, this weird [Circular] thing in module parent or children property. So, what is that?

Actually, [Circular] defines a circular reference, as in pic 2, which prints out the module object of operations.js. The parent property of the operations.js module references the app.js module.

Similarly, operations.js is a child module of app.js, so its children property should have a reference to the operations.js module. And, similarly, the operations.js module parent property again refers to the app.js module, so it will go into this infinite loop.

To prevent this infinite loop, Node sees that, if any module’s parent or child is already loaded, it will not load them again and show this [Circular] instead.

Pic 4

__filename

This is a variable that contains the absolute path of the current module.

Given two modules: a and b, where b is a dependency of a and there is a directory structure of:

  • /User/home/node_blog/a.js
  • /User/home/node_blog/node_modules/b/b.js

So, if we do console.log(__filename)within b.js, we will get /User/home/node_blog/node_modules/b/b.js. If we do console.log(__filename) within a.js, we will get /User/home/node_blog/a.js.

__dirname

The directory name of the current module. This is the same as the path.dirname() of the __filename.

So, for the above modules, a.js and b.js.

If we do console.log(__dirname) within b.js, we will get /User/home/node_blog/node_modules/b/ and in a.js, we will get /User/home/node_blog/.

Now we have studied the basics of the module. From now on, we will dive deep into this topic. Bear with me a bit longer as there are various interesting things we are going to discuss

Difference Between module.exports and exports

We use both module.exports and exports to export our functionalities out of our module.

But, there is a slight difference between them. Rather, I’ll say that they are not different but they are similar. The exports object is just shorthand for module.exports.

Inside Node, the exports object refers to the module.exports object. Which is somewhat like:

const exports = module.exports;

And, then, this module.exports object is returned by the require function when we require in a module.

And that is the reason we don’t change the reference of the exports object, as we wrote in Code 8 because, if we change the exports object, that will no longer refer to the module.exports, resulting in the functionalities not being exported from our module.

Can we use both module.exports and exports in a single module?

Yes, we can, but there are some subtleties we should keep in mind if we are using both.

Those are, when we use require in any module, we get the module.exports object and the exports object referring to module.exports, so it is necessary to maintain this reference.

In this code, the sum will not be exported as we have changed the reference of module.exports by assigning a new object to it but the exports object now also refers to the previous reference of module.exports.

To export the sum, we need to update the reference of the exports object to the current reference of module.exports.

Modules in Detail

It is not necessary that only a file can be a module that we require. Other than files, we also have folders as modules that we can require in.

Generally, a folder as a module is a module of modules, i.e. it contains various modules inside it to achieve functionality. This is what libraries do, they are organized in a self-contained directory and then they provide a single entry point to that directory.

There are two ways in which we can require a folder.

  1. Create a package.json in the root of the folder, which specifies a main module. An example package.json file might look like this:

If this was in a folder at ./some-library, then require('./some-library') would attempt to load ./some-library/lib/some-library.js.

This is the extent of Node.js awareness of package.json.

2. If Node does not find any package.json in the root directory of the module, or in package.json if the main entry is missing or cannot be resolved. Then, Node.js will try to load index.js or index.node from that directory. For example, if there was no package.json file in the above example, then require('./some-library') would attempt to load:

  • ./some-library/index.js
  • ./some-library/index.node

If these attempts fail, then Node.js will report the entire module as missing with the default error:

Error: Cannot find module ‘some-library’.

In file modules, .js file is also not the only module, we have .json files and .node files, they are also modules in Node.

Requiring in Detail

When we require a module it is not necessary to give the file extension.

For example, if there is a some-file.js file that we want to require and it is on the same level, we can require it as:

const someFile = require(‘./some-file’);

That is without specifying the extension.

While resolving the path of this file, Node follows a procedure.

It first looks for some-file.js, if some-file.js is not present, it will look for some-file.json and if that is also not present, it will look for some-file.node.

.js files are interpreted as JavaScript text files, and .json files are parsed as JSON text files, i.e. we get the JavaScript object. .node files are interpreted as compiled add-on modules.

How Does require Work?

When we require a module using the require function, it goes through various steps to import the required module.

  1. Resolving the module, i.e. finding the absolute path of the module.
  2. Loading the module in memory so that its code can be executed.
  3. Executing the code of the module.
  4. Bundling all exports from the module into one object module.exports.
  5. Caching the module, so that, when we require this module again, we don’t go over all these steps again and again.

Resolving the module goes through an order:

  1. First, it will look at whether the specified module in the resolve function is a core module or not. If it is a core module, it will return it and stop there only.
  2. Then, it will look at whether the path of the specified module starts with either /, ./, or ../, and if Node finds a match it will load it.
  3. If the module we are looking for is neither a core module nor starts with /, ./, or ../, then Node will start looking for our module in all the directories specified in the module.paths array (see pic 2 or 3) one-by-one. I.e. Node will start looking in our application parent directory node_modules folder and if it is not found there, it moves to the parent directory, and so on, until the root of the file system is reached.
  4. If Node is unable to find our module, it will throw an ‘unable to find’ error.

After resolving a module, Node will load it and executes its code, but what if we want to only check whether we have a particular module or not, i.e. we don’t want to load and execute it, we just want to resolve it.

For that we use the resolve function on require:

const resolvedFilename = require.resolve(moduleToResolve);

Use the internal require() machinery to look up the location of a module, but rather than loading the module, just return the resolved filename.

We have seen the order Node goes through to resolve a module and, also, it searches a path during the resolution of a module, as we discussed in point 3 above.

But, how do we programmatically know what all the paths are that Node searched to resolve our module? For that we use:

const allPaths = require.resolve.paths(moduleToResolve);

Returns an array containing the paths searched during the resolution of moduleToResolve or null, if the moduleToResolve string references a core module, for example, http or fs.

For example, for the following directory structure:

Directory Structure (Pic 5)

If we write and execute the following code :

Output of Code 14 will be:

Pic 6

Caching of Node Modules

Modules are cached after the first time they are loaded. This means (among other things) that every call to require('foo')will get exactly the same object returned, if it would resolve to the same file.

These modules are cached in an object in a key-value pair and we can reference that object using require.cache.

But, if we delete any key from this object, then require will reload the module next. There is also an exception with native addons. If we delete their key and try to reload them, it will result in an error.

For example, we have two files:

cacheFile1.js

console.log(‘Inside cache file 1’);

cacheFile2.js

console.log(‘Inside cache file 2’);
Output of code 15 (Pic 7)

In the output above, you can see that we get “Inside cache file 2” only one time, but we required it two times. This happens because, during the first time, only cacheFile1.js and cacheFile2.js are cached.

Checking Whether a Module Is a Main Module

A main module is the module that spun up your application. For example, if we start our application by writing node app.js, then app.js is the main module of my application.

We have one more property on require, called require.main, which is a module object referencing my main module, so if, in our code, we need to check whether the module is main or not and then we want to perform some actions, we can do:

if(require.main === module){
//Your Code goes here
}

Circular require

Pic 8

So, a circular require happens when a.js requires b.js and b.js requires a.js.

When there are circular require() calls, a module might not have finished executing when it is returned. Let’s take an example:

When main.js loads a.js, then a.js, in turn, loads b.js. At that point, b.js tries to load a.js.

To prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading and its exports object is provided to the a.js module.

By the time main.js has loaded both modules, they're both finished. The output of this program would thus be:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true

module Module in Node.js

There is also a module module inNode.js, which is different from the module object we have studied so far. It provides some general utility methods when we are interacting with our module.

For example, it provides one utility property, called builtinModules, to list out the name of all core modules of Node.js, which we can use to check whether the module we are using is Node’s core module or not, and perform actions accordingly.

const builtin = require('module').builtinModules;

Thank You

Thank you all for bearing with me till here. Various topics that we discussed here are not extensively used but, to excel in Node, we need to know how things actually work and the different functionalities they offer.

So, if you go and build an application, you have a better understanding of how modularity works and what all the things are that we can use.

References

Node.js v12.9.0 Documentation