Modules & npm
Modules let you split code into reusable files, and npm gives you access to the largest software registry in the world. Node supports two module systems—CommonJS and ECMAScript Modules—and understanding both is essential because you’ll encounter each in the wild.
CommonJS (require / module.exports)
CommonJS is Node’s original module system. You export with module.exports and import with require. It loads modules synchronously.
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// app.js
const { add } = require("./math");
console.log(add(2, 3));
Output:
5
require is synchronous, resolves paths relative to the current file, and caches each module after first load so repeated requires return the same instance.
ESM (import / export)
ECMAScript Modules are the standard JavaScript module system, shared with browsers. Use export and import. ESM is asynchronous and supports top-level await.
// math.mjs
export function add(a, b) {
return a + b;
}
// app.mjs
import { add } from "./math.mjs";
console.log(add(2, 3));
Enable ESM in one of two ways: name files .mjs, or set "type": "module" in package.json (then .js files become ESM).
| Feature | CommonJS | ESM |
|---|---|---|
| Syntax | require / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
| Top-level await | No | Yes |
| File hint | .cjs or default | .mjs or "type":"module" |
Prefer ESM for new projects—it’s the future standard and aligns Node with the browser. Reach for CommonJS only when a dependency or legacy codebase requires it.
package.json
Every Node project has a package.json manifest describing it. Create one with npm init -y.
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node app.js",
"dev": "node --watch app.js",
"test": "node --test"
},
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"eslint": "^9.13.0"
}
}
Installing packages and running scripts
npm install express # add a runtime dependency
npm install --save-dev eslint # add a dev-only dependency
npm install # install everything in package.json
npm run dev # run a script defined under "scripts"
npm start # shorthand for the "start" script
npm install writes a package-lock.json that pins the exact resolved versions of your entire dependency tree, guaranteeing reproducible installs across machines and CI.
Dependencies vs devDependencies
- dependencies — required at runtime in production (web frameworks, database drivers).
- devDependencies — only needed during development (linters, test runners, bundlers).
Running npm install --production or npm ci --omit=dev skips devDependencies, keeping production images lean.
Semantic versioning
Versions follow MAJOR.MINOR.PATCH, and range prefixes control which updates you accept:
| Range | Meaning | Allows |
|---|---|---|
^4.21.0 | Compatible with 4.x | >=4.21.0 <5.0.0 |
~4.21.0 | Patch updates only | >=4.21.0 <4.22.0 |
4.21.0 | Exact pin | only 4.21.0 |
A bump in MAJOR signals breaking changes, MINOR adds features backward-compatibly, and PATCH is bug fixes. The
^caret is npm’s default and a sensible choice for most dependencies.
Best Practices
- Commit
package-lock.jsonso everyone installs identical trees. - Use
npm ciin CI for clean, deterministic installs. - Keep secrets out of
package.json; use environment variables. - Prefer ESM for new code and avoid mixing module systems in one package.
- Audit regularly with
npm auditand update with intent, reading changelogs before major bumps. - Place build and dev tooling in
devDependencies, runtime libraries independencies.