Codepath

Modules & Packages

Overview

In node.js, a module is a file that exports a value (e.g., object, string, function, etc...) and can be required by other modules at runtime via the require(modulePath) function.

In node.js, a package, is a special case of module that when installed resides in the node_modules directory. Packages are different from modules in that they must have a package.json and can declare dependendencies, which npm will auto-install and resolve according to the versions specified in their package.json. Packages are required by name (e.g., require('bluebird')), and can be published for others to install.

Module Declaration

Export a single value:

When exporting a single value, your module.exports assignment should be at the top of your file just below the requires.

// greet.js
module.exports = function greet(name) {
  console.log(`Hello, ${name}!`)
}
// index.js
let greet = require('./greet')
greet('World') // Outputs: "Hello, World!"

Export a set of values:

When exporting an object or set of values, the module.exports should be the very last statement in the file.

// greet.js
function greet(name) {
  console.log(`Hello, ${name}!`)
}
function goodbye(name) {
  console.log(`Goodbye, ${name}!`)
}
module.exports = {greet, goodbye}
// index.js
let {greet, goodbye} = require('./greet')
greet('World') // Outputs: "Hello, World!"
goodbye('World') // Outputs: "Goodbye, World!"

Note: While use of the exports shorthand alias in place of module.exports is supported, it is discouraged due to issues that arise with circular or cyclic dependencies.

Require

Depending on the the argument, require follows 1 of 3 resolution patterns:

1. Core Modules

When the module name matches the name of a node.js core modules (e.g., path, fs, http) the core module will be returned:

let path = require('path')

2. File Modules

If the module contains a relative or absolute file path (i.e., by containing ./ or ../), the file is loaded based on that path. (See the export example above)

let path = require('./path')
In this case, even though path is a core module, the module at ./path.js, path.json, or path.node (in that order) is loaded instead.

Note: Don't name modules or packages names that conflict with core. For packages, they will be unrequirable, and for modules, they will cause confusion.

3. Packages

Lastly, if it's not a core module and contains no file delimiters, require will check for corresponding packages installed in the node_modules directory.

Note: 3rd party packages are one of node.js' greatest strengths. Checkout the npm registry for a searchable list.

Packages

Installation

To install the latest version of a package from the npm registry (e.g., fs-extra):

npm install fs-extra

Or, for a specific version:

npm install fs-extra@0.18.2

npm install will download and save the package in node_modules, and proceed to recursively install all the dependencies listed in the package's package.json's dependencies list.

global vs local packages

Packages can be installed "globally" with the -g flag. This can cause confusion, because global packages are not globally requirable. In node.js, a "global" package is a package that exposes a shell executable API (e.g., babel-node). If you need to require a package, you must install it locally.

package.json

Run npm init inside the project directory to create a package.json file, using the defaults results in the following:

{
  "name": "foo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
Typically, "name" and "version" are the most important properties followed by "main", "scripts" and "dependencies" (explained below). "name" is the name given to npm to install a package, and "version" is the version this install corresponds to.

Note: Typically, a starting version of 0.0.1 is more appropriate than 1.0.0 since your new package is probably not production ready.

"main"

The package entry point when required. (e.g., require('fs-extra') loads node_modules/fs-extra/lib/index). If "main" is empty, "index.js" will be used.

"scripts"

Used for CLI convenience commands, most commonly npm test and npm start. Additional keys can be added to "scripts" and run via npm run <script name>:

"scripts": {
  "start": "node index.js",
  "test": "mocha",
  "hello": "echo 'hello'"
}

In this case, npm run hello would run echo 'hello'.

"dependencies"

Since node_modules is ignored when publishing a package and is not typically checked-in, dependencies must be specified in the "dependencies" key. For convenience, use npm install --save <package name> to save the name and version in package.json:

npm install --save fs-extra
Now package.json includes:
"dependencies": {
  "fs-extra": "^0.18.2"
}

"devDependencies"

Use "devDependencies" for specifying dependencies that are only needed when running tests or builds. For convenience, use npm install --save-dev <package name> to save the name and version in package.json:

npm install --save-dev mocha
Now package.json includes:
"devDependencies": {
  "mocha": "^2.2.4"
}

Note: Unlike "dependencies", "devDependencies" are only installed when npm install is run from the package's root directory. In other words, a dependency's "devDependencies" is never installed.

Semver (Package Versioning)

In package.json "dependencies" include a three-number hierarchical version number spec called semver. (npm's semver documentation)

Hierarchy

The semver hierarchy is as follows, major.minor.patch:

  • Major: Breaking changes.
  • Minor: New features, functionality or APIs.
  • Patch: Bug fixes.

The best approach to versioning is to "trust but verify". When we trust publishers, the major version is usually all that matters. npm follows this rule by using the ^ range (see "Ranges" below) by default.

Ranges

Asterisk: *

"*" is synonymous with "latest". npm will install whatever the latest version is regardless of potential incompatibility. * can also be used for minor and patch versions: "1.3.*" or "1.*.*".

Caret: ^

^ is the default. npm will install the latest version with a matching major value. For example, "mocha": "^2.2.4" will allow mocha to be updated up to 3.0.0 non-inclusive, which denotes breaking changes.

Tilde: ~

~ is similar to ^, but for the latest minor version. For example, "mocha": "~2.2.4" will allow mocha to be updated up to 2.3.0 non-inclusive.

For more information the npm and package.json documentation.

References

How I Write Modules by Substack

Fork me on GitHub