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.jsonSource: 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!