1. Introduction đĻ
Every non-trivial Node.js application is built from modules â reusable pieces of code that can be shared, versioned, and composed. This tutorial covers how Node's module systems work, how npm resolves and manages dependencies, and how to publish your own packages.
Information
2. What are Modules? đ§Š
A module is simply a file (or set of files) whose code is encapsulated in its own scope. Variables and functions declared inside a module are private by default â they must be explicitly exported to be used elsewhere.
- Encourages separation of concerns and reusability.
- Avoids polluting the global namespace.
- Node supports two systems: CommonJS and ES Modules.
3. CommonJS Modules đ
CommonJS (CJS) is Node's original module system. Every .js file (without "type": "module") is treated as a CJS module, wrapped automatically in a function that provides require, module, and exports.
greet.js
function greet(name) {
return `Hello, ${name}!`;
}
module.exports = { greet };4. ES Modules đ
ES Modules (ESM) are the standardized JavaScript module format, also used in browsers. Node treats .mjs files â or .js files inside a package with "type": "module" â as ESM.
greet.mjs
export function greet(name) {
return `Hello, ${name}!`;
}Important
5. Core Syntax Reference đ¤
This section covers the individual keywords and functions used across both module systems.
5.1 require()
require() synchronously loads and returns the exports of another module. Results are cached â calling require() on the same path twice returns the same object.
app.js
const { greet } = require('./greet');
console.log(greet('Node'));5.2 module.exports
module.exports is the actual object returned by require(). You can assign to it directly to export a single value (a function, class, or object).
single-export.js
module.exports = function add(a, b) {
return a + b;
};5.3 exports
exports is a shorthand reference to module.exports. You can attach properties to it, but reassigning exports directly breaks the reference â always reassign module.exports instead.
exports-pitfall.js
exports.add = (a, b) => a + b; // â
works
exports = { subtract: (a, b) => a - b }; // â breaks the link to module.exportsCaution
5.4 import
import-example.mjs
import { greet } from './greet.mjs';
import defaultExport from './config.mjs';5.5 export
export-example.mjs
export const PI = 3.14159;
export function square(x) {
return x * x;
}5.6 Default Exports
A module can have one default export, imported without curly braces and under any name you choose.
config.mjs
export default {
port: 3000,
host: 'localhost',
};5.7 Named Exports
A module can have multiple named exports, each imported by its exact name (or renamed with as).
named-import.mjs
import { PI, square as sq } from './math.mjs';5.8 Dynamic Imports
import() is a function-like operator that asynchronously loads a module, returning a Promise. It works in both CJS and ESM contexts and is ideal for code-splitting or conditional loading.
dynamic-import.js
async function loadFeature(flag) {
if (flag) {
const { feature } = await import('./feature.mjs');
feature();
}
}6. Resolution đ
6.1 Module Resolution
When you require() or import a path, Node follows a specific resolution algorithm to locate the actual file.
6.2 Package Resolution
For bare specifiers like require('express'), Node searches for a node_modules folder in the current directory, then walks up the directory tree until it finds a match or reaches the filesystem root.
6.3 Local Modules
Local modules are files within your own project, referenced with relative paths (./utils, ../lib/db).
6.4 Third-Party Modules
Third-party modules are packages installed from a registry (typically npm) and live inside node_modules.
6.5 Built-in Modules
Node ships with built-in modules like fs, path, http, and crypto. These can be imported with or without the node: prefix (e.g. node:fs), which is recommended to avoid ambiguity with user packages of the same name.
builtin-example.js
const fs = require('node:fs');
const path = require('node:path');7. Package Configuration đī¸
7.1 package.json
package.json is the manifest file describing your project: its name, version, dependencies, scripts, and entry points.
package.json
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node --test"
},
"dependencies": {
"express": "^4.19.2"
}
}7.2 package-lock.json
package-lock.json records the exact version of every installed package (including nested dependencies), ensuring reproducible installs across machines and CI environments.
Best Practice
7.3 Semantic Versioning
Semantic Versioning (SemVer) follows the pattern MAJOR.MINOR.PATCH.
| Symbol | Meaning | Example |
|---|---|---|
| ^1.2.3 | Compatible with minor/patch updates | Allows 1.9.0, not 2.0.0 |
| ~1.2.3 | Allows only patch updates | Allows 1.2.9, not 1.3.0 |
| 1.2.3 | Exact version only | Only 1.2.3 |
| * | Any version | Not recommended |
8. Managing Packages đ§
8.1 Installing Packages
install.sh
npm install express # add as a dependency
npm install -D typescript # add as a devDependency
npm install # install everything from package.json8.2 Updating Packages
update.sh
npm outdated # see which packages have newer versions
npm update # update within semver ranges
npm install express@latest # force upgrade to latest8.3 Removing Packages
remove.sh
npm uninstall express8.4 npm Scripts
The scripts field in package.json lets you define shortcuts for common tasks, runnable via npm run <script>.
run-scripts.sh
npm run start
npm run test
npm run build8.5 Workspaces
Workspaces let you manage multiple related packages (a monorepo) from a single root package.json, sharing a single node_modules and linking local packages together.
package.json (root)
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}9. Publishing & CLI Packages đĸ
9.1 Publishing Packages
Publishing makes your package available on the npm registry for others to install.
9.2 Creating CLI Packages
A CLI package exposes an executable command. Add a bin field to package.json pointing to a script with a shebang line.
bin/cli.js
#!/usr/bin/env node
console.log('Hello from my CLI!');package.json
{
"name": "my-cli",
"bin": {
"my-cli": "./bin/cli.js"
}
}Tip
10. Best Practices â
- Pin dependency versions carefully; commit package-lock.json.
- Prefer node:-prefixed imports for built-in modules.
- Use exports field in package.json to control what's publicly accessible from your package.
- Avoid mixing CJS and ESM in the same package without clear "type" configuration.
- Run npm audit regularly to catch known vulnerabilities.
11. Common Mistakes â ī¸
- Reassigning exports instead of module.exports, silently breaking exports.
- Forgetting "type": "module" when using import/export syntax in .js files.
- Using overly loose version ranges (* or latest) in production dependencies.
- Deleting package-lock.json "to fix" install issues, causing irreproducible builds.
- Publishing a package without a .npmignore or files field, shipping unnecessary files.