DEV Community

Cover image for Node.js - module.exports vs exports
Thomas Finch
Thomas Finch

Posted on • Edited on

Node.js - module.exports vs exports

Photo by Julia Volk


I've recently started reading the excellent "Node.js Design Patterns" book by Mario Casciaro and Luciano Mammino. The second Chapter of Book discusses the Node.js module system and provides an explanation of the CommonJs modules system through a minimal example implementation.

Though a fairly straightforward concept I thought there was some potential for confusion regarding the discussion of module.exports versus exports and although the authors do address this I saw no harm in reiterating the point in this post.

We'll start by briefly covering the idea of software modules followed by looking at how to use the JavaSCript CommonJS module system with example implementations, and then finally we'll explore the important caveat of exports by taking a peek under the hood at how the CommonJS module system actually works.

1. So what are modules?

Modules are a key tenet behind programming languages they provide an explicit way of encapsulating data and functions from other areas of our code. They also encourage code re-usability and by wrapping modules into packages and libraries they provide a convenient way of sharing and distributing code.

In modern JavaScript there are two standard module systems, ECMAScript modules (ES modules) and CommonJS. Although CommonJS is certainly in decline relative to the introduction of ES modules in 2015 it is certainly still relevant in the current developer landscape and you have likely or will come across it in many Node.js code bases.

2. How do we import and export modules with CommonJS?

Two foundational concepts in CommonJS are module.exports and require. Simply put, module.exports allows us to export a module, and require is a resolving function that allows us to import an exported module from a specified file. The example implementation below demonstrates how we can export a single add function from the calculate.js file and import it into the main.js file.

// calculate.js

function add(num1, num2) {
    return num1 + num2;
}

module.exports = add;
Enter fullscreen mode Exit fullscreen mode
// main.js

const calculateAddition = require("./calculate");
const result = calculateAddition(2, 2);
console.log(result);

// result:
// 4

Enter fullscreen mode Exit fullscreen mode

Notice how when exporting via this method we lose the original exported function name add and instead, we pass a reference to the exported function to the variable calculateAddition. If we didn't want to lose the function name we could instead assign an object literal containing the add function to module.exports

// calculate.js

module.exports = {add};
Enter fullscreen mode Exit fullscreen mode

or equivalently we can use the dot notation to append an additional property to module.exports

// calculate.js

module.exports.add = add;
Enter fullscreen mode Exit fullscreen mode

To import and access this function we can use the following code

// main.js

const calculate = require("./calculate");
const result = calculate.add(2,2);
Enter fullscreen mode Exit fullscreen mode

Additionally, we can change the name of the exported function

// calculate.js

module.exports.addition = add;
Enter fullscreen mode Exit fullscreen mode
// main.js

const calculate = require("./calculate");
const result = calculate.addition(2,2);
Enter fullscreen mode Exit fullscreen mode

We can also easily export multiple functions, classes, primitives, and objects which are all accessible on the single exported object literal.

// calculate.js

function add(num1, num2) {
    return num1 + num2;
}

function sub(num1, num2) {
    // some code logic ...
}

function multiply(num1, num2) {
    // some code logic ...
}

const someConstantValue = 8;

const calculateObject = {
    a: 5,
    b: 6
};

class CalculationClass{
    calcSquareRoot(){
        // some code logic ...
    }
}



module.exports = {
    add,
    sub,
    multiply,
    someConstantValue,
    calculateObject,
    CalculationClass
}
Enter fullscreen mode Exit fullscreen mode

Anything that isn't explicitly exported remains only accessible in the file in which it was declared. This means it will be hidden from any other file importing it and allow us to encapsulate private logic and data, this is a common pattern in JavaScript and is known as the "revealing module pattern".

Instead of module.exports we could also use the alias exports though this has an important caveat which we will cover in the next section. An example of how we can export both a subtract and add function using the exports keyword can be seen below.

// calculate.js

exports.add = add;
exports.subtract = subtract;
Enter fullscreen mode Exit fullscreen mode

And to import and use these functions

// main.js

const calculate = require("./calculate");
const additionResult = calculate.add(2,2);
const subtractionResult = calculate.subtract(2,2);
Enter fullscreen mode Exit fullscreen mode

  

3. What is the important caveat of using 'exports'?

Let's go back to the first example where we assigned the add function to module.exports and try using the exports alias instead

// calculate.js

function add(num1, num2) {
    return num1 + num2;
}

// replacing
// module.exports = add;

// with
exports = add;
Enter fullscreen mode Exit fullscreen mode

If we attempt to import this in another file you'll get the following error


// main.js

const add = require("./calculate"); // 2.
const result = add(2, 2);
console.log(result);

// result:
// TypeError: add is not a function
Enter fullscreen mode Exit fullscreen mode

So why is that? To answer the question we'll need to take a brief look under the hood at how the module system in CommonJS works.

Every JS file in a Node.js application is actually a module. Before any code execution, Node.js will wrap all the code in each JS file inside a function wrapper. This function wrapper ensures that all functions, classes, variables, and objects remain private unless explicitly stated otherwise. Each function wrapper is passed the following parameters that are accessible to the code written in the file, exports, require, module, __filename, and __dirname .

The module parameter is simply an object that represents the current module and contains multiple properties including exports. The default value of the key exports in module is simply an empty object {}. As we saw in previous examples, export values can either be appended to this object using the dot notation

module.exports.add = add;
module.exports.sub = subtract;
Enter fullscreen mode Exit fullscreen mode

or can be assigned a new value e.g. Classes, Objects, functions, variables using

module.exports = add;

// or

module.exports = {add};
Enter fullscreen mode Exit fullscreen mode

So why can't we assign values directly to the exports parameter? Very simply this is because exports is a reference to the module.exports object. So we can append new properties to the module.exports object, for example using

exports.add = add;
exports.sub = sub;
Enter fullscreen mode Exit fullscreen mode

However, as the exports exports parameter is simply a reference if we attempt to assign it a new object using

exports = add;
Enter fullscreen mode Exit fullscreen mode

All we're doing is changing the reference of the object the exports parameters is pointing to rather than actually modifying the properties exported from the module.

4. Summary

  • Modules provide a convenient way for us to encapsulate our code within tightly coupled units that facilitate code reuse and sharing.

  • ES Modules and CommonJS represent the most common module systems used in modern JavaScript.

  • In CommonJS, we can use either module.exports or exports in any JS file to export values from a module and we use require to import values from another module.

  • Anything we don't explicitly export remains only accessible in the file in which it was declared, in other words 'encapsulated'.

  • module.exports represents the actual value of the exported values in a module and exports is simply a reference to module.exports.

  • We can append new values e.g. functions, objects, and primitives to either module.exports or exports using dot notation but we can only assign new values to module.exports.

  • Valid export examples

    • module.exports = {aFunction, anotherFunction}
    • module.exports = aFunction
    • module.exports.aFunction = aFunction
    • module.exports.renamedFunction = aFunction
    • exports.aFunction = aFunction
    • exports.aFunction = {aFunction}
  • Invalid export examples

    • exports = {aFunction, anotherFunction}
    • exports = aFunction

Top comments (0)