Skip to main content

module.exports vs exports

One of the most powerful things about software development is the ability to reuse and build upon the foundations of other people. This code sharing has helped software progress at an amazing rate.

Such a wonderful mechanism is critical on a micro-level for both individual projects and teams.

For Node.js, this process of code sharing – both within individual projects and in external npm dependencies – is facilitated using module.exports or exports.

How Node Modules Work

How do we use module exports to plug an external module, or sensibly break our project down into multiple files (modules)?

The Node.js module system was created because its designers didn't want it to suffer from the same problem of broken global scope, like its browser counterpart. They implemented CommonJS specification to achieve this.

The two important pieces of the puzzle are module.exports and the require function.

How module.exports works

module.exports is actually a property of the module object. This is how the module object looks like when we console.log(module):

Module {
  id: '.',
  path: '/Users/stanleynguyen/Documents/Projects/blog.stanleynguyen.me',
  exports: {},
  parent: null,
  filename: '/Users/stanleynguyen/Documents/Projects/blog.stanleynguyen.me/index.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/stanleynguyen/Documents/Projects/blog.stanleynguyen.me/node_modules',
    '/Users/stanleynguyen/Documents/Projects/node_modules',
    '/Users/stanleynguyen/Documents/node_modules',
    '/Users/stanleynguyen/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}

The above object basically describes an encapsulated module from a JS file with module.exports being the exported component of any types - object, function, string, and so on. Default exporting in a Node.js module is as simple as this:

module.exports = function anExportedFunc() {
  return "yup simple as that";
};

There's another way of exporting from a Node.js module called "named export". Instead of assigning the whole module.exports to a value, we would assign individual properties of the default module.exports object to values. Something like this:

module.exports.anExportedFunc = () => {};
module.exports.anExportedString = "this string is exported";

// or bundled together in an object
module.exports = {
  anExportedFunc,
  anExportedString,
};

Named export can also be done more concisely with the module-scoped exports predefined variable, like this:

exports.anExportedFunc = () => {};
exports.anExportedString = "this string is exported";

However, assigning the whole exports variable to a new value won't work (we will discuss why in a later section), and often confuses Node.js developers.

// This wont work as we would expect
exports = {
  anExportedFunc,
  anExportedString,
};

Imagine that Node.js module exports are shipping containers, with module.exports and exports as port personnel whom we would tell which "ship" (that is, values) that we want to get to a "foreign port" (another module in the project).

Well, "default export" would be telling module.exports which "ship" to set sail while "named export" would be loading different containers onto the ship that module.exports is going to set sail.

My "flagship" analogy for Node.js module.exports' role

Now that we have sent the ships sailing, how do our "foreign ports" reel in the exported ship?

How the Node.js require keyword works

On the receiving end, Node.js modules can import by require-ing the exported value.

Let's say this was written in ship.js:

...
module.exports = {
  containerA,
  containerB,
};

We can easily import the "ship" in our receiving-port.js:

// importing the whole ship as a single variable
const ship = require("./ship.js");
console.log(ship.containerA);
console.log(ship.containerB);
// or directly importing containers through object destructuring
const { containerA, containerB } = require("./ship.js");
console.log(containerA);
console.log(containerB);

An important point to note about this foreign port operator – require – is that the person is adamant about receiving ships that were sent by module.exports from the other side of the sea. This leads us to the next section where we will address a common point of confusion.

module.exports vs exports – What is the difference and which do you use when?

Now that we have gone through the basics of module exporting and requiring, it's time to address one of the common sources of confusion in Node.js modules.

This is a common module exports mistake that people who are starting out with Node.js often make. They assign exports to a new value, thinking that it's the same as "default exporting" through module.exports.

However, this will not work because:

  • require will only use the value from module.exports
  • exports is a module-scoped variable that refers to module.exports initially

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

If you want to learn more about this technical explanation, the Node.js official documentation is a good place to start.

Back to the analogy that we made previously using ships and operators: exports is another port personnel that we could inform about the outgoing ship. At the start, both module.exports and exports have the same piece of information about the outgoing "ship".

But what if we tell exports that the outgoing ship will be a different one (that is, assigning exports to a completely new value)? Then, whatever we tell them afterwards (like assigning properties of exports to values) won't be on the ship that module.exports is actually setting sail to be received by require.

On the other hand, if we only tell exports to "load some containers on the outgoing ship" (assigning properties of exports to value), we would actually end up loading "containers" (that is, property value) onto the ship that is actually being set sail.

Based on the common mistake explained above, we could definitely develop some good conventions around using CommonJS modules in Node.js.

Node.js export best practices – a sensible strategy

Of course the convention offered below is entirely from my own assessments and reasonings. If you have a stronger case for an alternative, please don't hesitate to tweet me @stanley_ngn.

The main things I want to achieve with this convention are:

  • eliminating confusion around exports vs module.exports
  • ease of reading and higher glanceability with regards to module exporting

So I'm proposing that we consolidate exported values at the bottom of the file like this:

// default export
module.exports = function defaultExportedFunction() {};
// named export
module.exports = {
  something,
  anotherThing,
};

Doing so would eliminate any disadvantages in terms of conciseness that module.exports have versus shorthand exports. This would remove all incentives for us to use the confusing and potentially harmful exports.

This practice would also make it very easy for code readers to glance at and learn about exported values from a specific module.

Going beyond CommonJS

There's a new, and better (of course!) standard that's recently been introduced to Node.js called ECMAScript modulesECMAScript modules used to only be available in code that would eventually need transpilation from Babel, or as part of an experimental feature in Node.js version 12 or older.

It's a pretty simple and elegant way of handling module exporting. The gist of it can be summed up with the default export being:

export default function exportedFunction() {}

and the named export looking like this:

// named exports on separate LOC
export const constantString = "CONSTANT_STRING";
export const constantNumber = 5;
// consolidated named exports
export default {
  constantString,
  constantNumber,
};

These values can then easily be imported on the receiving end, like this:

// default exported value
import exportedFunction from "exporting-module.js";
// import named exported values through object destructuring
import { constantString, constantNumber } from "exporting-module.js";

This results in no more confusion from module.exports vs exports and a nice, human-sounding syntax!

Comments

Popular posts from this blog

Breaking Down Scope, Context, And Closure In JavaScript In Simple Terms.

Breaking Down Scope, Context, And Closure In JavaScript In Simple Terms. Breaking Down Scope, Context, And Closure In JavaScript In Simple Terms. “JavaScript’s global scope is like a public toilet. You can’t avoid going in there, but try to limit your contact with surfaces when you… Breaking Down Scope, Context, And Closure In JavaScript In Simple Terms. Photo by Florian Olivo on  Unsplash “ J avaScript’s global scope is like a public toilet. You can’t avoid going in there, but try to limit your contact with surfaces when you do.” ― Dmitry Baranowski Here’s another (much) more simple article I wrote on the subject: Closures In Javascript Answer A closure is a function defined...

links

links Absolutely Everything You Could Need To Know About How JavaScript TOC & Condensed Links **** **** **** **** **** 1 2 3 4 5 leonardomso/33-js-concepts *This repository was created with the intention of helping developers master their concepts in JavaScript. It is not a…*github.com Call stack - MDN Web Docs Glossary: Definitions of Web-related terms MDN *A call stack is a mechanism for an interpreter (like the JavaScript interpreter in a web browser) to keep track of its…*developer.mozilla.org Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more *Web developers or Front end engineers, as that’s what we like to be called, nowadays do everything right from acting as…*medium.com Understanding the JavaScript call stack *The JavaScript engine (which is found in a hosting environment like the browser), is a single-threaded interpreter…*medium.freecodecamp.org Javascript: What Is The Execution Context? ...

Bash Proficiency

Bash Proficiency In Under 15 Minutes Bash Proficiency In Under 15 Minutes Cheat sheet and in-depth explanations located below main article contents… The UNIX shell program interprets user commands, which are… Bash Proficiency In Under 15 Minutes Cheat sheet and in-depth explanations located below main article contents… The UNIX shell program interprets user commands, which are either directly entered by the user, or which can be read from a file called the shell script or shell program. Shell scripts are interpreted, not compiled. The shell reads commands from the script line per line and searches for those commands on the system while a compiler converts a program into machine readable form, an executable file. LIFE SAVING PROTIP: A nice thing to do is to add on the first line #!/bin/bash -x I will go deeper into the explanations behind some of these examples at the bottom of this article. Here’s some previous articles I’ve written for more advanced users. Bash Commands That Sa...