Understanding Node.js Modules

Understanding how module exports and require works is essential towards understanding Node.js.

Understanding Node.js Modules

When I started learning about Node.js modules, I remember being confused about them.

Modules are self-contained entities that provide distinct functionality, allowing them to be changed, added, or removed as needed in order to form a complete system.

Modules are essential for splitting your code into reusable components. They're such a simple concept, yet the underlying mechanism is so complex.

First thing you'll need to know is that all the dependencies in Node.js are handled using two core built-in objects:

  • The require object ― available across your Node.js application as a function
  • The module object ― because each file represents a module, it is local and individual to each file

The module object is usually used for exporting things from a file, and the require function is used for importing the exported things into a file.

Module exports

You can think of exporting as choosing what values (just about anything) you're making available from the current file within another file. You define what gets exported using the module.exports object.

module.exports = {
  greeting: 'Hello Node.js!'
};

Using module.exports vs exports

Something I've encountered when looking at other projects is the keyword exports by itself. The two are one and the same object, exports being a simple reference to module.exports.

Node.js makes the exports variable available behind the scenes by doing the following:

var exports = module.exports = {};

Module importing

Whenever you require a file, Node.js goes through a series of actions that will ensure your file is loaded properly:

  • Resolving the path ― First, Node.js determines the absolute path of the file you're requiring, relative to the file that you're importing in.
  • Loading the file ― Next, Node.js determines what type of content the file contains. You can also require .json files and Node.js knows how to differentiate the two based on the provided file extension.
  • Scoping the code ― The require and module objects are local to the file they're in. Each individual file has it's own scope.
  • Evaluating the code ― Next, the Virtual Machine evaluates the loaded code inside the newly created scope, limited the file where you're importing it in.
  • Caching the file ― To achieve better performance, Node.js caches the loaded file. Once you've require a file, it will be ready to be reused.
const importedModule = require('./path/to/file');

Resolving module paths

During the path resolving step, Node.js gives you a lot of flexibility. It's important that you understand the difference between requiring paths and requiring packages.

You should keep in mind that you don't need to specify the .js extension, because Node.js's path resolving mechanism adds it automatically.

Another thing I found very useful when writing Node.js applications is that you can specify a folder path in your require statement, and then the index.js file inside the specified folder will be targeted.

require('./path/to/file');

The require statement above, used with a relative path, will be resolved for you in one of the following ways:

// The path refers a .js file
require('/home/user/path/to/file.js'); 

// The path refers to a folder
require('/home/user/path/to/file/index.js');

If the specified path is nor a relative or an absolute path, it will be treated as a npm package. You'll learn more about creating and requiring npm packages in the next tutorial.

require('package-name');

Export and require in action

Understanding how module exports and imports work together is essential towards understanding Node.js. The way Node.js handles modules work is fairly complicated, but the interface for using modules is not complicated at all.

When I started out, I understood what require() does. What I didn't know, however, was about the existence of the module.exports object and how it interferes with requiring modules. One thing I really take pleasure in doing is learning through practical examples, so let's write a simple program.

Let's write a small file called greetings.js, where we'll export a simple function for saying hello.

// File: greetings.js

function sayHello (name) {
  console.log('Hello ' + name);
}

function sayGoodbye (name) {
  console.log('Goodbye ' + name);
}

module.exports = {
  sayHello: sayHello,
  sayGoodbye: sayGoodbye
};

Next, let's import and use the greeting inside an index.js file. Make sure to place the two files in the same folder.

// File: index.js

const greetingsModule = require('./greetings');

greetingsModule.sayHello('Node.js');
greetingsModule.sayGoodbye('Node.js');

Last but not least, run the program as you've learned in the previous tutorial.

node index.js

What's next?

This is merely an introduction to how module exporting and importing works. There's a lot more to learn about it, and I'll try my best to bring you up to speed with the best practices on the topic.

I've covered fundamental functionality such as requiring npm packages and creating your own package in All About NPM Packages. Make sure you read it!

Read up next