Understanding Node.js Module Exports

The capacity to reuse and build upon the foundations of other people is one of the most powerful aspects of software development. The code-sharing has helped in the rapid advancement of software. This process of code sharing in Node.js is eased via module.exports or exports, which can be used both within individual projects and in external npm dependencies.

Modules are code structure building elements that help Node.js developers better structure, reuse, and distribute code. A module is a self-contained code block that can be included anywhere we need it in a file or directory. Modules and the module system are integral to the development and structure of Node.js applications.

This article will go through what module exports are, why they're important, and how to export and import them into your Node.js project.

Here’s how it’s done:

  1. What is a Module?
  2. Types of Modules
  3. Role of Modules in JavaScript
  4. Which Module System is the Most Effective?
  5. What are Node.js Module Exports?
  6. module.exports vs exports
  7. What's the Best Way to Organize Your Module Exports?
  8. What are the Benefits of Modules?

What is a Module?

Modularity is a common programming strategy that can be found in almost all of the best software projects, regardless of programming languages or software stacks.

Decomposing a monolithic piece of logic or functionality into separate, individual, independent components that work together in unison but are much easier to manage, update, maintain, troubleshoot, and debug is known as modularizing your code.

The concept of modularity fits in perfectly with the well-known Single Responsibility Principle (SRP) and "don't repeat yourself" (DRY) philosophies in software development. They propose avoiding redundancy by focusing each sub-program component on only one part of the functionality.

Most programming languages may take advantage of the benefits of native and third-party open-source libraries and frameworks produced by their communities due to this modularity.

Developers and organizations can use modules to bundle software features and functionalities, share them, and import, use, and expand them in a simple plug-and-play method. Modules have aided in the advancement of software development and the creation of dependable applications.

Assigning values to the module.exports make them available for use in other sections of the project. Modules can be reused as needed and serve to manage the codebase of the applications. The creation of modules for specialized tasks helps in the maintenance of clean code.

Types of Modules

There are three different types of modules in Node.js:

  1. Built-in Modules
  2. Local Modules
  3. External Modules
  1. Built-in Modules
    The built-in modules come packaged with Node.js. A separate installation is not required. You use require keyword to load them. The Node.js standard library is made up of these built-in modules. The built-in modules of Node.js are part of the language and are developed by the core Node.js team.
  2. Local Modules
    These are modules that you write yourself, and they are checked into version control as part of your actual codebase. The use of local modules in your project allows you to reuse code. Creating a file for utilities is one example. The code in that file can then be exported and used in various parts of your application.
  3. External Modules
    NPM packages are what external modules are. An external module is installed as a dependency and stored in the node_modules/ directory, which is tracked in your package.json. Since the reference is managed with the package.json file, the actual code of an external module is not checked into version control.

Role of Modules in JavaScript

Javascript began as a simple web programming language; applications were not yet the full-fledged behemoths they are today. As a result, modularity may not have ranked higher on the priority list.

However, as the language increased in popularity and was eventually replaced by Node.js, this demand arose as applications became more complex and Javascript could execute outside of the browser.

Workarounds such as anonymous functions and global objects would not suffice. As a result, over the years, there have been various attempts to introduce structures to enable modularity in Javascript programming.

In the world of Javascript, these enhancements come in a variety of formats. Here are a few of the more modern formats that have made it possible for developers to include modules in their code in a variety of ways:

  • In browsers, the Asynchronous Module Definition (AMD) format is used to define modules using a defined function.
  • Node.js employs the CommonJS (CJS) standard to define dependencies and modules, which uses require and module.exports. This format is the foundation of the npm ecosystem.
  • The ESM (ES Module) format. JavaScript now supports a native module format as of ES6 (ES2015). It employs the export and import keywords to export and imports the public API of a module.
  • The System.register format was created to allow ES6 modules to run alongside ES5.
  • Both the browser and Node.js support the Universal Module Definition (UMD) standard. It comes in handy when a module needs to be imported by a variety of module loaders.
Note: CommonJS is the Node.js standard module specification.

Which Module System is the Most Effective?

We've covered AMD, CommonJS, and even native JavaScript modules as examples of existing module syntaxes. But which is the most effective? And why would we need to utilize CommonJS in the first place?

The answer is that it is debatable.

For performance optimizations in the browser, AMD is useful. CommonJS makes a lot of sense with Node.js, but JavaScript modules still need a pre-compiler. Native JavaScript modules, on the other hand, are an excellent alternative for both. That's because, even if you're pre-compiling today, you'll most likely be able to complete all of your pre-compilation processes in the near future.

However, there is a solution for software developers, and it's called UMD (a Universal Module Definition). A JavaScript module can be compatible with all of the other module formats due to UMD.

What are Node.js Module Exports?

To grasp the module object interface, you'll need to know how to export and import objects in Node.js.

#1 The Module Object

Each code file is recognized by Node.js as a separate module. Each runnable file gets its module object, which represents the module itself.

Each module's object (also known as a free variable) is unique. It includes details such as the filename, path information, exported variables, and more. More information about the module object can be found here.

In your terminal, open the node command line and print the module object to see what it's all about.

$ node
> console.log(module)
Module {
    id: '<repl>',
    exports: {},
    parent: undefined,
    filename: null,
    loaded: false,
    children: [],
    paths: ['/home/ubuntu/myproject/node_modules',
        '/home/ubuntu/node_modules',
        '/home/node_modules',
        '/node_modules',
        '/home/ubuntu/.node_modules',
        '/home/ubuntu/.node_libraries',
        '/usr/local/lib/node'
    ]
}
undefined

Alternatively, you can log this from a file and see if the 'filename' variable now has a value.

// sample.js
console.log(module)

The output looks like this:

 $ node sample.js
Module {
    id: '.',
    exports: {},
    parent: null,
    filename: '/home/ubuntu/path/to/sample.js',
    loaded: false,
    children: [],
    paths: ['/home/ubuntu/path/to/node_modules',
        '/home/ubuntu/node_modules',
        '/home/node_modules',
        '/node_modules'
    ]
}

#2 module.exports

The exports property of the module object is an object in and of itself. It contains all of the variables, objects, and functions from our current module that we want to export. As a result, we can add other (current module) objects to it that we want to be available from other files.

Let's have a look at how this works in code when we want to export a few basic variables. We just add our variables to the exports object and check for consistency.

// sample.js
var num = 24
var text = 'Hello!!!'
console.log("Before Exporting: ", module.exports)

module.exports.num = text // exporting pre-defined var
module.exports.text = text
module.exports.foo = 74 // defining var to be exported on the fly
console.log("After Exporting: ", module.exports)

The output looks like this:

// sample.js
$ node sample.js
Before Exporting: {}
After Exporting: {
    num: 24,
    text: 'Hello!!!',
    foo: 74
}

The modularity principles allow you to split down your application's source code into smaller, more manageable functions and objects that we can share and connect with the rest of the project.

The following are the two methods for exporting your objects:

1) Named Exports (multiple)

So far, these are the ones we've been looking at. Everything we want to export gets a unique identifier (the object's key in module.exports) that we can use to import it.

module.exports.num = num
module.exports.text = text
module.exports.foo = 74

When importing these variables, we can use the identifiers num, text, and foo. This enables us to export a large number of objects from our module.

2) Default Export (single, unnamed)

The other types of imports are more fundamental. Module.exports are, as we know, just another object; instead of adding items to it (as we did before), we can now initialize the entire thing.

// sample.js
var num = 24
var text = 'Hello!!!'
module.exports.num = num
module.exports.text = text
module.exports.foo = 74
console.log("Before: ", module.exports)
module.exports = {
    "foo": 10,
    "bar": 20,
    "baz": 30
} // overriding the exports object
console.log("After: ", module.exports)

The output looks like this:

$ node sample.js
before: {
    num: 24,
    text: 'Hello!!!',
    foo: 74
}
after: {
    foo: 10,
    bar: 20,
    baz: 30
}

We can also export functions in this way –

module.exports = () => {
    console.log("Hello!!!")
}

When the module only has one primary function or object to export, we choose this option. Based on how we export these objects, we can then import them into other files (either with or without named identifiers). Let's look at how to use the require keyword to import from these modules.

#3 Require

The CommonJS specification provides the require keyword syntax for importing built-in modules, third-party modules (loaded via NPM), and other custom modules in your project that you've exported using module.exports.

let myModule = require(name)

A file or directory in the same folder, a built-in core module, or a node module package can all be used as the name. In a simple example, let's swap things between multiple files. Assuming you've saved your variables in a single file:

// sample.js
var num = 24
var text = 'Hello!!!'
module.exports.num = num
module.exports.text = text
module.exports.foo = 74

You can use the require keyword to import them into another file (in the same directory):

// example.js
var bar = require('./sample.js').num // importing specific object as any variable we like
var baz = require('./sample.js').text
var imported_obj = require('./sample.js') // importing the whole exported obj
var foo = imported_obj.foo
// displaying all imports
console.log(bar)
console.log(baz)
console.log(foo)
console.log("Whole imported object: ", imported_obj)

Our second file, example.js, is now being executed.

$ node example.js
24
Hello!!!
74
Whole imported object: {
    num: 24,
    text: 'Hello!!!',
    foo: 74
}

As previously stated, require imports one of the following items:

  • Core modules with the provided name (e.g. http, url, querystring, path, etc.)
  • The package of node modules with the specified name has been installed
  • The given name of a file or directory
  • If none of these matches, it throws an error

We can also apply a clever destructuring assignment operator on several objects in a single line. This is how it seems:

var {
    num,
    text,
    foo
} = require('./sample.js')

As expected, we must import the variables with the same object names as when they were exported.

module.exports vs exports

Now that we've covered the fundamentals of module exporting and requiring, let's look at one of the most prevalent causes of Node.js module confusion.

This is a typical module exports blunder made by those new to Node.js. They presume that assigning exports to a new value is the same as "default exporting" via module.exports.

This, however, will not function because of the following reasons:

  • Only the value from the module will be used by require
  • exports is a module-scoped variable that originally refers to the module.exports

We're effectively pointing the value of exports to another reference away from the initial reference to the same object as module.exports by assigning it to a new value.

The official documentation for Node.js is a fantastic place to start if you want to learn more about this technical explanation.

It doesn't matter which you use because module.exports and exports both point to the same object. Consider the following scenario:

exports.foo = 'foo';
module.exports.bar = 'bar';

The exported object of the module would be foo:'foo', bar:'bar' using this code.

There is, however, a catch. What is exported from your module is whatever you assign module.exports to.

As an example, consider the following:

exports.foo = 'foo';
module.exports = () => {
    console.log('bar');
};

This would only result in the export of an anonymous function. The foo variable isn't going to be used.

What's the Best Way to Organize Your Module Exports?

So now you know that you can use several pieces of code to modify your module exports object. However, we never specified how you should organize your files. Since you have so much flexibility, it might be a little confusing. Do you create your properties before? Isn't everything at the conclusion of the file? What is the industry standard?

We'll get to these questions right now.

Exporting as You Go vs. At the End of a File

The fact that we can apply properties to our exports object raises the question of when we should do so.

It's perfectly OK to use properties throughout your module, as shown below:

module.exports.getUser = () => {
    // Code here
}
module.exports.getUsers = () => {
    // Code here
}

This would return the following if you console-logged it as an import:

{
    getUser: [Function],
    getUsers: [Function]
}

It's worth noting that our functions are anonymous because we used the fat arrow function (which can be problematic when trying to decipher stack traces).

As a result, many Node.js developers prefer the following pattern:

javaScriptfunction getUser() {
    // Code here
}

function getUsers() {
    // Code here
}
module.exports = {
    getUser,
    getUsers
}

This would result in:

{
    getUser: [Function: getUser],
    getUsers: [Function: getUsers]
}

This provides us with the names of the functions and properly explains the API at the conclusion of the file.

Overall, the revealing module pattern is a well-known pattern among JavaScript developers.

What are the Benefits of Modules?

This fragmentation offers many benefits:

  • It makes it easier to manage and maintain your project because failure areas are simple to identify and solve.
  • You can swap out code components and try out new features without compromising core functionality because of the flexibility.
  • Reusing and extending previously implemented code, minimizes redundancy and boosts efficiency.
  • Unit testing and debugging are much easier.
  • It saves you time and resources, both in the short and long term.
  • It is safer and more reliable to prevent a problem in one element of the system from spreading to other parts.

Conclusion

I hope this article has provided you with a solid overview of how to work with modules in Node.js. Modules have become an important aspect of the JavaScript ecosystem, allowing us to build massive programs from smaller components. It's as simple as keeping your files separate and cleanly documenting your code through the appropriate structure to make you and your team more productive when writing Node.js code.


Monitor Your Node.js Applications with Atatus

Atatus keeps track of your Node.js application to give you a complete picture of your clients' end-user experience. You can determine the source of delayed response times, database queries, and other issues by identifying backend performance bottlenecks for each API request.

To make bug fixing easier, every Node.js error is captured with a full stack trace and the specific line of source code marked. To assist you in resolving the Node.js  error, look at the user activities, console logs, and all Node.js requests that occurred at the moment. Error and exception alerts can be sent by email, Slack, PagerDuty, or webhooks.

Try Atatus’s entire features free for 14 days.

Atatus

#1 Solution for Logs, Traces & Metrics

tick-logo APM

tick-logo Kubernetes

tick-logo Logs

tick-logo Synthetics

tick-logo RUM

tick-logo Serverless

tick-logo Security

tick-logo More

Janani
Janani works for Atatus as a Content Writer. She's devoted to assisting customers in getting the most out of application performance management (APM) tools.
India