2019-02-11
|~9 min read
|1601 words
Update With Node 13, modules came out from behind the experimental feature flag. So How Do We Use Modules In Node.js? has been updated to reflect the new steps.
On more than one occasion I’ve found myself looking up the MDN page on imports and exports for modules in Javascript.
This is often preceded or followed shortly by an investigation into the different between the import/export and requirement statements in Javascript.
Hopefully this is the last time that I have to do this exercise, but either way, I wanted to have a single place to look.
Preethi Kasireddy wrote a great guide for freeCodeCamp on Modules - what they are and why we use them. Read it. I particularly liked her analogy for how to think about modules:
Good authors divide their books into chapters and sections; good programmers divide their programs into modules.
Modules have several advantages:
The most common (pun intended) way I’ve found to import a module is with the CommonJS approach. This is where Javascript exports module objects which are then accessible for other modules to require.
ES6 added import / export statements as well. This standard was set without native support in Javascript engines and so tools like Babel would transpile an import
into a require
statement.
To use modules natively in Node.js, we have several options (as explained by Axel Rauschmayer) at the end of 2018:
esm
library by John-David Dalton—experimental-modules
flagUpdate for 2021
To use modules within Node v13+,
- save the file with the extension
.mjs
- add { “type” : “module” } in the nearest
package.json
Source: Stack Overflow
The short answer then is: If you’re not transpiling with Babel or Webpack, using the esm
library or the --experimental-modules
flag, you need to use require
statements.
In the Node.js context, we have a few different ways to “bundle” a module together to be reference-able by other modules.
There’s also distinctions between export
and module.exports
that are worth discussing.
Let’s see these in action!
In order to understand exports, it’s helpful to think about the require
statement.
A require
statement tells a module what to consume. These can be built into Node.js (as in the case of fs
below), local (like the ones we’ll be defining later in utils.js
), or community libraries (stored in the node_modules
directory).
In this example, we’re declaring that our file has a dependency on Node’s filesystem module (fs
) and we will be accessing it through the variable fs
.
const fs = require("fs")
fs.readFile("./file.txt", "utf-8", (err, data) => {
if (err) {
console.error(err)
} else {
console.log(`data: `, data)
}
})
There are a number of ways to export modules for access but they can be bucketed into exports
and module.exports
.
Simply put - exports
is an alias for module.exports
with the caveat that assigning directly to exports
will break the link to module.exports
(More on this below.)
The number of ways to export functions, classes, generators, etc. are many and varied. MDN has the full list of acceptable syntax (copied below for convenience):
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function FunctionName(){...}
export class ClassName {...}
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
exports.fn = () =
export function fn(){}
The following shows three different ways to export functions. The first two use function declaration and function expressions. The third is a mixture but uses the module.exports to bundle the functions for access later.
// named function declaration
export function add(x, y) {
return x + y
}
export function diff(x, y) {
return x - y
}
// alternative approach with ES6 function expressions
exports.add = (x, y) => {
x + y
}
exports.diff = (x, y) => {
x - y
}
// alternative approach with module.exports
function add(x, y) {
return x + y
}
const diff = (x, y) => {
x - y
}
module.exports = { add, diff }
Accessing these functions would be done using a require
statement (unless using the --experimental-modules
flag or esm
library, in which case you could use import
).
For example, regardless of how we export the utils
module, we could import it into main.js
in the following way:
const utils = require("./utils")
console.log(`add 2 and 3 --> `, utils.add(2, 3))
console.log(`the difference between 3 and 1 -->`, utils.diff(3, 1))
Alternatively, if we did not want to have to reference the utils
namespace, we could do the following:
const { add, diff } = require("./utils")
console.log(`add 2 and 3 --> `, add(2, 3))
console.log(`the difference between 3 and 1 -->`, diff(3, 1))
This is possible because we used named exports.
If we’re using modules, we could also do:
import * as utils, {add} from "./utils";
console.log(`add 2 and 3 --> `, add(2, 3))
console.log(`the difference between 3 and 1 -->`, utils.diff(3, 1))
If we’re not exporting functions, we may want to export Classes, or objects, etc. Here’s how that could look:
class Sample {
doSomething() {
console.log(`done`)
}
}
module.exports = Sample // with only one property, do not use brackets
// alternative example with multiple classes
class Sample {
doSomething() {
console.log(`done`)
}
}
class Second {
doSomethingElse() {
console.log(`yessir`)
}
}
module.exports = { Sample, Second }
These can then be accessed in the following way:
// main.js
// single class example
const Sample = require("./classes")
const sample = new Sample()
sample.doSomething()
// multiple class example
const Classes = require("./classes")
const sample = new Classes.Sample()
const second = new Classes.Second()
sample.doSomething()
second.doSomethingElse()
There’s also a Default option. If you’re only exporting a single method, class, etc, this can make sense as you can name it on import.
export default function add(x, y) {
return x + y
}
This could then be imported as:
const utils = require("./utils")
console.log(utils.default(2, 3))
If you have multiple exports, however, while I prefer naming all exports, there’s the option to mix and match.
That said, there can only be one default per export in a module.
For example:
// making the add method our default
export default function add(x, y) {
return x + y
}
export function diff(x, y) {
return x - y
}
const PI = 3.1415
export const area = (r) => {
return PI * r ** 2
}
export const circumference = (r) => {
return 2 * PI * r
}
These could then be imported into main.js
and accessed in the following way:
const utils, { diff, circumference, area} = require('./utils')
utils.default(2,3) // this is the add method
diff(5,2)
From the Node.js docs
[exports]
allows a shortcut, so thatmodule.exports.f = …
can be written more succinctly asexports.f = …
. However, be aware that like any variable, if a new value is assigned toexports
, it is no longer bound tomodule.exports
. Consequently the following will not work.
While it’s okay to mix Default and Named exports, mixing exports
and module.exports
is not going to work well.
For example:
exports.add = (x, y) => {
return x + y
}
diff = (x, y) => {
return x - y
}
module.exports = { diff }
const add = (x, y) => {
return x + y
}
exports = add // add won't be a function when the module is required later.
Hopefully this was helpful and serves as a good jumping off point for anyone else who might have been confused about what modules are and how to use them.
I definitely am indebted to all of the people who came before to write down their thoughts.
Articles:
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!