package.json
dependencies— needed at runtime, installed in production.devDependencies— needed only to build or test.npm ci --omit=devskips them.scripts—npm run dev.startandtestmay drop therun.engines— a stated requirement; add it so a wrong Node version fails loudly.
{
"name": "doors-api",
"version": "1.4.2",
"type": "module",
"private": true, // stops an accidental publish
"engines": { "node": ">=20" },
"scripts": {
"dev": "node --watch src/app.js",
"start": "node src/app.js",
"test": "node --test",
"lint": "eslint ."
},
"dependencies": { "express": "^4.19.2" },
"devDependencies": { "eslint": "^9.9.0" }
}
Semantic versioning
1.4.2
│ │ └── patch: a bug fix, nothing else changed
│ └──── minor: something new, everything old still works
└────── major: something broke on purpose
| Range | Means | Accepts |
|---|---|---|
^1.4.2 | Compatible: no major change | 1.4.2 → 1.9.9, not 2.0.0 |
~1.4.2 | Patches only | 1.4.2 → 1.4.9, not 1.5.0 |
1.4.2 | Exactly that | only 1.4.2 |
>=1.4 <2 | Written out | as it reads |
* | Anything | good luck |
Zero is special
Below 1.0.0 the API is not stable, so ^0.2.3 allows 0.2.9 but not 0.3.0 — the minor position behaves like a major one. This surprises people once, usually during an outage.
The lockfile is the truth
package-lock.jsonis committed. It records the exact version of every package, including your dependencies’ dependencies.- Use
npm ciin CI and in deployment: it is faster and it cannot drift. node_modulesis never committed..gitignoreit on the first commit.
npm install # resolves the ranges, may pick newer versions, WRITES the lockfile
npm ci # installs exactly what the lockfile says, deletes node_modules first
The commands you actually use
npm install express # add a dependency
npm install -D vitest # add a dev dependency
npm install # install everything in package.json
npm update # move up within the ranges
npm outdated # what is behind, and how far
npm uninstall express
npm run dev
npx create-vite@latest # run a package without installing it
npm audit # known vulnerabilities in the tree
Before adding a dependency
Every dependency is code you ship without reading, from someone who can publish a new version tonight. That is a reasonable trade — it is just not a free one.
- When was it last published? Three years is not always abandoned, but check.
- How big is it, and what does it drag in? A package with forty dependencies is forty things that can break.
- Could you write it in twenty lines? Sometimes yes — and then you own twenty lines instead of a supply chain.
- Is it already in Node?
fetch,crypto.randomUUID,structuredClone, a test runner and a.envloader are all built in now.
Scripts worth having
Anyone who clones the repository should be able to run npm install && npm run dev and have it work. If that needs a paragraph of instructions, the scripts are wrong.
{
"scripts": {
"dev": "node --watch --env-file=.env src/app.js",
"start": "node src/app.js",
"test": "node --test",
"test:watch": "node --test --watch",
"lint": "eslint .",
"format": "prettier --write ."
}
}