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;
// main.js
const calculateAddition = require("./calculate");
const result = calculateAddition(2, 2);
console.log(result);
// result:
// 4
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};
or equivalently we can use the dot notation to append an additional property to module.exports
// calculate.js
module.exports.add = add;
To import and access this function we can use the following code
// main.js
const calculate = require("./calculate");
const result = calculate.add(2,2);
Additionally, we can change the name of the exported function
// calculate.js
module.exports.addition = add;
// main.js
const calculate = require("./calculate");
const result = calculate.addition(2,2);
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
}
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;
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);
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;
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
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;
or can be assigned a new value e.g. Classes, Objects, functions, variables using
module.exports = add;
// or
module.exports = {add};
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;
However, as the exports
exports parameter is simply a reference if we attempt to assign it a new object using
exports = add;
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
orexports
in any JS file to export values from a module and we userequire
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 andexports
is simply a reference tomodule.exports
.We can append new values e.g. functions, objects, and primitives to either
module.exports
orexports
using dot notation but we can only assign new values tomodule.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)