How the Node Module System Works
In this tutorial, learn how the file-based Node.js module system works, and about core, local, and third-party modules. As building blocks of code structure, Node.js modules allow developers to better structure, reuse, and distribute code. A module is a self-contained file or directory of related code, which can be included in your application wherever needed. Modules and the module system are fundamental parts of how Node.js applications are written and structured.
Every JavaScript file in Node.js can be considered a module, and each file can export code which can be used by other parts of an application.
In this tutorial we’ll:
- Explore module and package architecture, and how the module system helps us write better code
- Understand core modules, local modules, and third party modules
- List different ways to export code from a module
- Review how the require function or
require
call works
Goal
Understand how Node’s module system helps us write reusable code.
Prerequisites
- None
Watch: Node’s Module System
What is a module?
A module is unit of code, organized into a file or folder, that we export functionality from so that we can include it in other parts of our application. In Node.js, each file has a global object in its scope called module
, which holds information about that specific file. The module
variable is an object, which has a property called exports
. Assigning values to module.exports
will expose them for inclusion in other files.
Modules let us write code once, and then reuse it where it is needed. This is useful for organizing our code, and creating modules for accomplishing specific tasks. Modules written by other developers are typically distributed publicly and for free as packages on NPM. We are able to include other developers’ modules in our code by installing them from NPM, and passing the package name to the require
function to load the module. We can use local modules we’ve created in our codebase directly from the filesystem by passing the module’s file path to the require
function.
Not every file needs to be used as a module or use module.exports
, though, as not every file contains code that is used in other parts of the application. Modules are created specifically to expose code to other parts of your application. For example, you’re likely to only start your server listening for requests in one place in the application, and don’t need to export the code for reuse elsewhere. You certainly could use it elsewhere if you wanted, but starting an application generally only needs to be done in one place. One of the big benefits of using modules is reducing duplication of code, so any general purpose code like utility functions are a great candidate for exporting to be reused elsewhere.
If you inspect the module
object, you will see something like this which describes a file:
Module { id: '.', exports: {}, parent: null, filename: '/Users/jon/Projects/heynode.com_content/index.js', loaded: false, children: [], paths: [ '/Users/jon/Projects/heynode.com_content/node_modules', '/Users/jon/Projects/node_modules', '/Users/jon/node_modules', '/Users/node_modules', '/node_modules' ] }
As you can see, the module
object holds information about the file, including an exports
object.
Types of modules
Node.js has three main types of modules to work with:
- Built-in modules
- Local modules
- External modules
Each one functions similarly to the others, and relies on the module system. They differ only in their origins.
Built-in modules are distributed with Node.js itself; you don’t need to install them from anywhere. You load them into your code using require
. These built-in modules make up the slim standard library of Node.js — the minimal core pieces of language functionality that are shipped with Node.js itself and available for use across all projects. An example of a built-in module is the http
module, which can be used to create an HTTP client or server. These modules are developed by the core Node.js team, and are part of the language itself. These modules won’t change unless they are changed in a new release of Node.js.
Local modules are modules you write yourself, and are part of your actual codebase. This is how you reuse code you’ve written in your project. An example would be creating a file for utilities you’ve written and that are specific to the application. You can then export code from that file to be used in different parts of your application. The actual code in local modules are written by you, and it gets checked into version control because it’s a part of the codebase.
External modules can be summed up as NPM packages. An external module is installed as a dependency, tracked in your package.json, and located in your node_modules/ directory. The module’s code itself isn’t checked into your version control system or part of your codebase. Instead, we track a reference to the module and associated version in package.json. When using a package from NPM, you’re using an external module.
How do we use the module system?
The module system lets us break up our code into files or folders that are organized by functionality. To export code from a file for use in another file, we assign values to the file’s module.exports
object. The module system lets you encapsulate a unit of code and expose it to be reused by other parts of your application, almost like building blocks.
The module system extends the CommonJS standard. In the early days of Node.js, there was no explicit module system that the language used, so CommonJS was adopted and extended to fill the gap and set forth a standard the community can use. This tutorial focuses on the CommonJS style implementation, as it is the standard module system for Node.js at the time of writing.
If the NPM registry is the heart of the Node.js ecosystem, then CommonJS and the module system are the backbone.
Here is a simple example of a module which exposes a single function by overwriting the module.exports
object:
const multiplyByTwo = function(x) { return x * 2};
module.exports = multiplyByTwo;
Above, we have exported a single function from a file called math.js, by assigning the function to module.exports
. The function takes a number, multiplies by 2, and returns the result.
In any given file, we can assign a value to module.exports
, and then include that value elsewhere by passing the file’s path to the require
function. The require
function loads a file or package and returns the value assigned to module.exports
.
Only the code assigned to module.exports
is exported from a file, while the rest of the code in the file remains in the private scope of the file. The exported code still has access to the private scope of the file it was exported from because the module acts as a closure around the code in the file. This is useful for exposing only the interface we want to from a module, keeping any additional private logic safely in the scope of the module. require
can accept a path to a file (relative or absolute) or module identifiers for packages installed via npm.
To use the module we created, we require it in another file.
const multiplyByTwo = require('./math.js');
console.log(multiplyByTwo(10));// 20
// here we can see what was returned by requiring the moduleconsole.log(multiplyByTwo);// [Function: multiplyByTwo]
To export multiple values from a file, assign an object as the value of module.exports
. You can then access the properties of the object after requiring the module.
const add = function(x, y) { return x + y};const subtract = function(x, y) { return x - y};const multiplyByTwo = function(x) { return x * 2};
module.exports = { add, subtract, multiplyByTwo};
const math = require('./mathFunctions.js');
const sum = math.add(2, 2);const doubled = math.multiplyByTwo(sum);console.log(doubled);// 8
Another way to export multiple values from a file is by using the global exports
object which is an alias of module.exports
and also available in each file.
This code snippet is functionally the same as the previous one which used module.exports
:
exports.add = function(x, y) { return x + y};exports.subtract = function(x, y) { return x - y};exports.multiplyByTwo = function(x) { return x * 2};
Either style works, just don’t mix them in the same file or else you might lose the reference to the previous exports
values by assigning a new value to module.exports
:
exports.add = function(x, y) { return x + y};exports.subtract = function(x, y) { return x - y};exports.multiplyByTwo = function(x) { return x * 2};
module.exports = "I just lost my previous exports! Don't do this";
Because exports
is set initially as a reference to module.exports
, values set on the exports
object will be transferred to the module.exports
object. However, setting exports
to an entirely new value will assign a new reference to exports
and no longer point to module.exports
.
console.log(exports === module.exports);// true
exports = 'foo'
console.log(exports === module.exports);// false
What is require
?
Exporting code using module.exports
is only half of the module system. You need a way to include the code in other parts of the application. You can do that with the require
function.
In order to load a local module, we pass its relative file path to the require
function, which returns the value of module.exports
from the file. This is actually very similar to what happens when you require an npm module. Except in this instance, we are passing require
a path to a file instead of the name of a package. When you require an npm package, the same thing is happening behind the scenes in your node_modules/ directory.
A module is evaluated the first time it is passed to the require
function. The code in the file will be wrapped in a private scope, run, and the value of module.exports
is returned by require. After the module has been required once, the module is cached, and requiring the file again will return the cached result, without evaluating the file again.
Here is an example:
console.log('I was required!');
module.exports = "I'm an exported string!";
Then our sampleModule.js file is required twice in another file. (This is just for demonstration, don’t do this in your actual code):
const sampleValue = require("./sampleModule.js");// "I was required" is logged from the sampleModule file
const secondSampleValue = require('./sampleModule.js');// nothing is logged
The first time the sampleModule.js file is required, the console.log
will be run, but the second time nothing will be logged to the console, since the module has already been evaluated and cached by the first require.
You can also pass a directory path to require instead of a filepath. If require
is passed a path to a directory, it will look for an index.js file in the directory, and use that as the entrypoint to the module.
If you have a directory called myModule/ that contains an index.js file that exports code, you can load the module like so:
const myModule = require('./myModule');
Require will find the index.js file inside myModule/ and load the module. This is a useful way to organize your application. Modules can keep all code in a single directory. It’s mostly stylistic, but can be very useful when a single module is composed of multiple different files, with a single index.js file as the point where the code is exported from.
But what about the import
and export
keywords?
You may have also heard about ESM (EcmaScript Modules), and seen code that uses import
and export
keywords when dealing with modules. This is the next generation module system, which is only currently supported behind experimental flags of the latest Node.js releases, or by using a transpiler like Babel to convert the ESM import
and export
code into regular CommonJS format through adding a build step to the project.
To be clear, ESM is not yet natively supported in Node.js (as of Node.js v12), and the CommonJS based standard is currently what we mean when we talk about the module system. CommonJS uses require
and module.exports
, and ESM uses import
and export
keywords to work with modules. That’s a gross oversimplification, but it’s also how you can tell them apart.
When developing applications for the frontend, it’s standard to use something like Babel to transpile your code before being deployed to make it compatible with as many browsers as possible. That’s why why you see the experimental import
and export
syntax being used in codebases. Before the code is actually put into production, the import
and export
statements are replaced with CommonJS require
and module.exports
by Babel. But when developing for the backend, there is no need for transpilation because we control the environment our code runs in, and the extra step of transpiling to improve compatibility isn’t nearly as useful.
In this tutorial we’ve chosen to focus on the CommonJS format of modules, as they are currently the only natively supported module system in Node.js. ESM is the future of modules in Node.js, but CommonJS modules aren’t going away anytime soon. It’s important you clearly understand how they work first before working with ESM.
Recap
The module system helps us reuse code throughout our application. A module is a self-contained file or directory of related code which can be included anywhere in an application by passing the file path to the require
function. Using the global module
object or the exports
object, we can expose code from a file for inclusion elsewhere, while keeping some parts of the module encapsulated in their own private scope. Project-agnostic modules are published on the NPM registry as packages for reuse across projects.
Further your understanding
- How would you share a local module with other developers?
- Take a look at an NPM package’s source code and see how they organize their code and use the module system. For example, look how express organizes their code into modules and exposes them.
Additional resources
- About modules (docs.npmjs.com)
- Node.js Documentation: ECMAScript Modules (nodejs.org)
- npm-publish: Publish a package to the registry (docs.npmjs.com)