Node.js Module Exports Explained

As Donald Knuth very wisely put it, computer programming is an art. And an integral aspect of writing clean code and staying true to this art is modularity.

This post will cover the use of modules in Node.js — what they are, why they are essential, and how to export and import them in your project. 

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the post – 

What are Modules?

Modularity is one of the most popular programming practices that you’ll never fail to find in almost all of the top software projects across all programming languages and software stacks.

Modularizing your code means decomposing a monolithic piece of logic or functionality into separate, individual, independent components that work together in unison but are much easier to organize, update, maintain, troubleshoot, and debug. This fragmentation has several benefits: 

As far as software development is concerned, the concept of modularity ties nicely with the renowned Single Responsibility Principle (SRP) and “don’t repeat yourself” (DRY) philosophies. They advocate that each sub-program component should focus on only one aspect of the functionality and avoid redundancy. You can read more about the top five S.O.L.I.D. programming principles in this post on our blog.

Thanks to this modularity, most programming languages can benefit from the virtues of native and third-party open source libraries and frameworks developed by their communities. Through modules, developers and organizations can segregate software features and functionalities into packages, share them, and easily import, utilize, and extend these in a simple plug-and-play fashion.

Modules have been vital in advancing software development and developing reliable applications.

The Role Modules Play in Javascript

Now let’s talk about the role of modules in the context of Javascript and Node.js.

Javascript started as a small programming language for the web; applications weren’t as full-fledged behemoths as they are these days. As a result, modularity might not have been above “get the language up and ready for building the web” on the priority list. However, as the language gained popularity (to the point of becoming the most commonly used programming language in the world) and eventually made way for Node.js, this requirement emerged as applications grew more complex and Javascript could run outside the browser. Workarounds using anonymous functions and global objects were not going to be enough. As a result, there have been several attempts at introducing structures for enabling modularity in Javascript code over the years. 

In the world of Javascript, these additions come through different specification formats. Here are some of the recent formats that have enabled developers to leverage modules in their code in different ways —

CommonJS is the standard module specification for Node.js. To stick to the topic of this post, we’ll leave the other formats for now and solely focus on the module.exports (and require) syntax introduced by CJS.

What are Module Exports in Node.js?

Understanding the export and import of objects in Node.js is vital to understand the module object interface that allows this.

The Module Object

Node.js acknowledges each code file as a separate module. As you’ll see below, each runnable file gets its own module object that represents the module itself. This object (also known as a free variable) is local to each module. It contains information like the filename, path information, exported variables (the one we’re currently most interested in), and more. You can read more about the module object here.

Fire up the node command line in your terminal, and let’s 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/username/repl/node_modules',
    '/home/username/node_modules',
    '/home/node_modules',
    '/node_modules',
    '/home/username/.node_modules',
    '/home/username/.node_libraries',
    '/usr/local/lib/node' ] }
undefined


You can also log this from a file and observe the ‘
filename’ also get a value this time.

// hello.js
console.log(module)

OUTPUT:

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

module.exports

As we saw above, the exports property of the module object here is an object in itself. It contains all the variables, objects, and functions we want to export from our current module. Therefore, we can add other objects (in the current module) to it that we want to be accessible from other files. Let’s see what this looks like in code when we want to export a few simple variables. Here, we just add our variables to the exports object and verify the same. 

// hello.js
var my_int = 20
var my_str = 'Hello world!'

console.log("before exporting: ", module.exports)

module.exports.my_int = my_int // exporting pre-defined var
module.exports.my_str = my_str
module.exports.foo = 50 // defining var to be exported on the fly

console.log("after exporting: ", module.exports)

OUTPUT:

// hello.js
$ node hello.js
before exporting:  {}
after exporting:  { my_int: 20, my_str: 'Hello world!', foo: 50 }

As you can see, we can use the module.exports object as a dictionary that stores key-value pairs of things you want to export. We can also similarly export functions:

function my_func() {
        console.log("Jell-o world!")
}

console.log("before exporting: ", module.exports)

module.exports.my_func = my_func
module.exports.my_other_func = () => { // using arrow notation
        console.log("Fellow world!")
}

console.log("after exporting: ", module.exports)

OUTPUT:

$ node hello.js
before exporting:  {}
after exporting: { my_func: [Function: my_func], my_other_func: [Function]}


Aligned with the principles of modularity, this allows you to break down your application’s source code into smaller, more easily manageable functions and objects that we can share and integrate with the rest of the project.
 

Here are the two different ways of exporting your objects:


Named (multiple) exports: These are the ones we have been looking at so far. Everything we want to export here gets an identifier (i.e., the object’s key inside module.exports) that we can use when importing them.

module.exports.my_int = my_int
module.exports.my_str = my_str
module.exports.foo = 50

Here, my_init, my_str, and foo are the identifiers we can use while importing these variables. This allows us to export multiple objects from our module.

Default (single, nameless) export: The other kind of imports are more basic. As we know, module.exports is just another object; instead of adding items to it (like before), we can also initialize the whole thing as we like.

// hello.js
var my_int = 20
var my_str = 'Hello world!'

module.exports.my_int = my_int
module.exports.my_str = my_str
module.exports.foo = 50

console.log("before: ", module.exports)

module.exports = {"foo":20, "bar": 40, "baz":60} // overriding the exports object

console.log("after: ", module.exports)

OUTPUT:

$ node hello.js
before:  { my_int: 20, my_str: 'Hello world!', foo: 50 }
after:  { foo: 20, bar: 40, baz: 60 }

Similarly, we can also export functions as such –

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


We go for this case when the module contains only one primary function or object we want to export. We can then import these objects in other files based on how we export these objects (either with or without named identifiers). Now, let’s look at importing from these modules using the
require keyword.

Require

For importing built-in modules, third-party ones (you’ve installed through NPM), and other custom ones in your project that you’ve exported using module.exports, the CommonJS specification offers the required keyword syntax. 

let my_import = require(name)

Here, the name can be of a file or directory in the same folder, a built-in core module, or a node module package. Let’s exchange objects between multiple files in a small example. Assuming you have exported your variables as such in one file –

// hello.js
var my_int = 20
var my_str = 'Hello world!'

module.exports.my_int = my_int
module.exports.my_str = my_str
module.exports.foo = 50


You can import them in another file (in the same directory) using the
require keyword as such –

// jello.js

var bar = require('./hello.js').my_int // importing specific object as any variable we like
var baz = require('./hello.js').my_str

var imported_obj = require('./hello.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)

Now we run our second file, jello.js.

$ node jello.js
20
Hello world!
50
Whole imported object: { my_int: 20, my_str: 'Hello world!', foo: 50 }


As I mentioned before,  require imports one of the following –

It throws an error if none of these match.

There’s also a fancy destructuring assignment operator that we can use to multiple objects in one line. This is what that looks like – 

var {my_int, my_str, foo} = require('./hello.js') // one line import

Here, as expected, we need to import the variables using the same object names used while exporting them.

Before we dive into a sample use case, let’s explore some other variants you’ll find across the internet.

module.exports vs exports (alias)

You might also have seen developers using just exports, instead of module.exports

By definition, exports is a shortcut/reference/alias variable assigned to the value of module.exports in the beginning (before evaluation).

There’s some unnecessary confusion that this causes, especially for inexperienced developers. Many think of exports and module.exports as interchangeable. And this even makes sense – if you try to play with the former a little bit, this interchangeable theory holds, and it does seem to be a convenient shortcut to the latter. Let’s see how. We’ll print the two at the beginning of a piece of code, then update both, one by one, and verify if their counterparts are updated or not.   

// hello.js

// 1
console.log("1. At the beginning:")
console.log('module.exports', module.exports)
console.log('exports', exports) // should be same as module.exports

console.log('––––––––')



// 2

module.exports.foo = 'bar' // updating module.exports
console.log("2. After updating `module.exports`:")
console.log('module.exports', module.exports)
console.log('exports', exports) // should be same as module.exports
console.log('––––––––')



// 3

exports.baz = 'bax'
console.log("3. After updating `exports`:") // updating exports
console.log('module.exports', module.exports) // should be same as next line's output
console.log('exports', exports)


OUTPUT:

$ node hello.js
1. At the beginning:
module.exports {}
exports {}
––––––––
2. After updating `module.exports`:
module.exports { foo: 'bar' }
exports { foo: 'bar' }
––––––––
3. After updating `exports`:
module.exports { foo: 'bar', baz: 'bax' }
exports { foo: 'bar', baz: 'bax' }

As you can see, both hold the same values when we add an item for export. This is because exports starts as a reference to module.exports, i.e., it refers to module.exports, initially. Even as we keep adding objects for exporting, exports can still keep pointing to module.exports and therefore serve as a helpful shortcut.

However, this changes when we assign module.exports a new value (instead of merely updating the existing one) as such –

module.exports = () => { // assigning a new object as in default exports
console.log("Hello world!")
}


or

module.exports = { // assigning a new object as in default exports
foo: ‘bar’,
      baz: 20
}


When this happens, the exports variable isn’t updated with the newly assigned value because it is still a reference (pointing) to the previous/initial
module.exports value. Let’s extend the previous example to see this in action –

// hello.js



// 1

console.log("1. At the beginning:")
console.log('module.exports', module.exports)
console.log('exports', exports) // should be same as module.exports

console.log('––––––––')



// 2
module.exports.foo = 'bar' // updating module.exports
console.log("2. After updating `module.exports`:")
console.log('module.exports', module.exports)
console.log('exports', exports) // should be same as module.exports
console.log('––––––––')



// 3
exports.baz = 'bax' // updating exports
console.log("3. After updating `exports`:")
console.log('module.exports', module.exports) // should be same as exports
console.log('exports', exports)
console.log('––––––––')



// 4 (this is where the discrepancy starts)
module.exports = {my_str:'Hello world!'} // assigning a new object value
console.log("4. After reinitialising (assigning a new object to) `module.exports`:")
console.log('module.exports', module.exports)
console.log('exports', exports)  // exports will not have updated with the recent value (would still hold the old module.exports value)
console.log('––––––––')



// 5

console.log("5. After updating `module.exports`:")
module.exports.choo = 'bar'
console.log('module.exports', module.exports)
console.log('exports', exports) // still does not update
 


Here, we update sections 1, 2, and 3 like before, but assign a new value to
module.exports in section 4 to see how that changes things and update it again in section 5. Here’s what the output for this looks like –

OUTPUT: 

$ node hello.js
1. At the beginning:
module.exports {}
exports {}
––––––––
2. After updating `module.exports`:
module.exports { foo: 'bar' }
exports { foo: 'bar' }
––––––––
3. After updating `exports`:
module.exports { foo: 'bar', baz: 'bax' }
exports { foo: 'bar', baz: 'bax' }
––––––––
4. After reinitialising (assigning a new object to) `module.exports`:
module.exports { my_str: 'Hello world!' }
exports { foo: 'bar', baz: 'bax' }
––––––––
5. After updating `module.exports`:
module.exports { my_str: 'Hello world!', choo: 'bar' }
exports { foo: 'bar', baz: 'bax' }

As you can see, after section 3, the exports alias doesn’t update with the recently assigned module.exports value. After module.exports gets a new value altogether,  it no longer remains in sync with the exports variable (which now refers to a stale version of what we want to export). As a result, in sections 4 and 5, the exports value doesn’t update despite the changes in module.exports.

Even though exports seems to be a more convenient shortcut and would work just fine if you only update your exports (instead of giving the object a new value), you must be aware of one caveat: the value of module.exports is eventually exported and made available to import from other modules.

Which should I use?

TL;DR: Although using exports is a convenient shortcut, it might be safer to opt for module.exports because of the discrepancies caused when assigned a new value. In that case, it might require extra work to keep the two variables in sync.

To learn more about module.exports and its shortcut, I recommend reading the official Node.js documentation here. 

Import/Export Syntax with ES6 in Javascript

In the world of Javascript, the language specifications and standard formats keep advancing and allow the introduction of new improvements in its utility and performance. 

With the ES6 (ECMASCRIPT 2015) standard, Javascript also supports the import/export keywords for importing and exporting modules between files. It is fair to say that this syntax seems more intuitive than the module.exports/require one we have been discussing so far. 

//hello.js
export function my_function () {
    console.log("Hello world")
}

// jello.js
import my_function as my_func from 'hello.js'




my_func()

However, Node.js’s original module format has been CommonJS, and therefore, it treats JavaScript code as CommonJS modules by default. For Node.js versions between 8 - 12, you can enable the experimental ES6 modules’ support feature by renaming your file’s extension to .mjs and running your file as –

node –experimental-modules my-file.mjs

Although, support for ES modules comes enabled by default only in Node.js versions 13 onwards. You can read more about using ES6 modules in Node.js (version >= 13) here.

How to Do a Node.js Module Export: An Example

Now that we have some context about modules in Node.js, let’s walk through a simple example of how module exports work end-to-end. To demonstrate this, we’ll create a dummy application to simulate the manufacturing, export, and import of arbitrary items (through modules) across multiple cities (files). Unlike the examples above where we just exported simple strings and functions, we’ll move around classes and objects to give a sense of how we use modules in practice.

Architecture diagram of our example application (arrows indicate the direction of export)

{Step 1} Create your Modules and Export Them

Define items, manufacturing processes, hubs, and export!

Based on the above architecture diagram, we’ll organize our modules in the following folder structure –

cityA/
hub.js
item.js
manufacturing.js
cityB/
hub.js

Let’s start by defining the item as a class with a constructor and a helper function. After defining the class with appropriate class variables, we will export this for instantiating later.

// cityA/item.js

class Item {
  constructor(name, type, weight) {
      this.name = name
      this.type = type
      this.weight = weight
      this.manufacturingProcess = null // to be updated later
  }

  getItemInfo() { // helper function for printing item info
      return `
          Name: ${this.name}
          Type: ${this.type}
          Weight: ${this.weight}
          Manufacturing Process: ${this.manufacturingProcess}
      `;
  }
}

module.exports = Item // exporting our item class


Next, let’s simulate a bunch of arbitrary manufacturing processes through functions in the
manufacturing.js file. Logically, these functions will take in the item class objects we defined before, change some of their properties, and return them.

// cityA/manufacturing.js




function manufacturingProcessA (item) {
  item.name += '_A' // modifying item name based on process
  item.manufacturingProcess = 'A'
  item.weight /= 2 // arbitrary modification
  return item
}

function manufacturingProcessB (item) {
  item.name += '_B'
  item.manufacturingProcess = 'B'
  item.weight /= 3
  return item
}

function manufacturingProcessC (item) {
  item.name += '_C'
  item.manufacturingProcess = 'C'
  item.weight /= 4
  return item
}

module.exports = { // exporting functions for use
  manufacturingProcessA,
  manufacturingProcessB,
  manufacturingProcessC
}


Now we have our item and manufacturing modules prepared and exported. We’ll import these into a module we’re going to call the hub in
cityA/hub.js.

// cityA/hub.js

const Item = require('./item.js') // importing the item class
const { manufacturingProcessA, manufacturingProcessB, manufacturingProcessC } = require('./manufacturing.js') // importing manufacturing processes


var foo = new Item('foo', 'typePQR', 10) // creating new item
const manufacturedFooA = manufacturingProcessA(foo) // manufacturing the item

foo = new Item('foo', 'typePQR', 10)
const manufacturedFooC = manufacturingProcessC(foo) // same item manufactured differently

const bar = new Item('bar', 'typeXYZ', 30)
const manufacturedBarB = manufacturingProcessB(bar)

// exporting manufactured items ->

module.exports.fooA = manufacturedFooA
module.exports.fooC = manufacturedFooC
module.exports.barB = manufacturedBarB

Note how we used the destructuring assignment operator above to import multiple manufacturing process functions. We also import (require) the file using ‘./’ before the filename to specify the file inside the same folder (instead of the project’s root directory).

We then instantiated item objects, passed them through arbitrary manufacturing processes, and eventually exported them (from city A).

{Step 2} Import!

Now that our manufactured goods are on their way from city A’s hub, let’s import them in city B’s.

// cityB/hub.js
const {fooA, fooC, barB} = require('../cityA/hub.js') // importing manufactured items (note the relative path to the hub)

console.log("Imported at City B's hub: ")

console.log(fooA.getItemInfo()) // printing item information to verify
console.log(fooC.getItemInfo())
console.log(barB.getItemInfo())

Now that our code is ready, let’s test the manufacturing, export, and import pipeline to see if everything works as expected.

OUTPUT:

$ node cityB/hub.js
Imported at City B's hub:

            Name: foo_A
            Type: typePQR
            Weight: 5
            Manufacturing Process: A
       

            Name: foo_C
            Type: typePQR
            Weight: 2.5
            Manufacturing Process: C
       

            Name: bar_B
            Type: typeXYZ
            Weight: 10
            Manufacturing Process: B

This confirms that our goods have successfully reached city B’s hub!

Where Best to Use Module Exports?

There are two ways in which you can position your export statements – (1) exporting after (or as) you define each object or (2) exporting them all at once at the end, as shown below –

// (1) exporting as you go

const foo = {}
module.exports.foo = foo

const boo = {}
module.exports.boo = boo

module.exports.my_func = () {
// function is anonymous (can't be used in this file)
}
// (2) exporting all at once at the end

const foo = {}
const boo = {}
function my_func () {
...
}

module.exports.foo = foo
module.exports.boo = boo
module.exports.my_func = my_func

Developers generally favor placing all exports at the end instead of exporting as you go for the following reasons –

Also, keep in mind that it might be safer to use only module.exports in most places instead of occasionally switching it with its alias if you are unsure of any new assignments to module.exports.

Over to You 

Now that you have a good understanding of modules and their export/import practices in Node.js, go ahead and evaluate how you modularise your code and consider leveraging well-organized modules for your next Node.js project.

If you are interested in reading more articles around software and web development, subscribe to our newsletter, and check out our blog. Also, consider monitoring your application’s performance using Scout so you can spend less time debugging and more time building!