Chapter 3 of 17
~11 minnpm, package.json & Semantic Versioning
npm is the default package manager for Node.js, and package.json is the manifest that describes your project, its dependencies, and how to run it. Reading version ranges and lockfiles correctly saves you from a lot of "works on my machine" bugs.
Anatomy of package.json
package.json records project metadata (`name`, `version`, `description`), the `scripts` map that npm run/start/test invoke, the `main` (or `exports`) field describing your package's entry point, and two dependency lists. It is both a manifest for npm and, for a published package, the contract other projects rely on when they install it.
{
"name": "my-api",
"version": "1.4.2",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest"
},
"dependencies": {
"express": "^4.19.2",
"dotenv": "~16.4.5"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.1.0"
},
"engines": {
"node": ">=18"
}
}dependencies vs devDependencies
`dependencies` are packages your running application needs in production - a web framework, a database driver. `devDependencies` are only needed while developing - test runners, linters, bundlers. Running `npm install --production` (or setting `NODE_ENV=production` in older npm versions) skips devDependencies, which keeps deployed images smaller and avoids shipping tooling you don't need at runtime.
A common mistake is installing a runtime necessity like `express` with `--save-dev`, which works fine locally but breaks a production install that skips devDependencies.
Semantic Versioning
SemVer version numbers follow `MAJOR.MINOR.PATCH`. A MAJOR bump signals breaking changes, MINOR adds backward-compatible functionality, and PATCH is a backward-compatible bug fix. The caret `^4.19.2` allows updates that do not change the leftmost non-zero digit - so npm may install `4.20.0` or `4.99.9` but never `5.0.0`. The tilde `~16.4.5` is stricter: it only allows PATCH-level updates, so `16.4.9` is fine but `16.5.0` is not.
Pinning an exact version (no `^` or `~`) guarantees byte-for-byte reproducibility for that one package, but you lose automatic bug-fix updates and have to bump it manually.
"^4.19.2" // >=4.19.2 <5.0.0 (minor and patch updates allowed) "~16.4.5" // >=16.4.5 <16.5.0 (patch updates only) "4.19.2" // exactly 4.19.2, no updates "*" // any version at all (risky - avoid in real projects)
Lockfiles Keep Installs Deterministic
package.json describes acceptable ranges, but `package-lock.json` (or `yarn.lock`/`pnpm-lock.yaml`) records the exact resolved version - and exact dependency tree - that was installed last time. Committing the lockfile means every teammate and every CI run installs identical versions, even though the ranges in package.json would technically permit newer ones.
Running `npm ci` instead of `npm install` in CI/deploy pipelines installs strictly from the lockfile and fails fast if package.json and the lockfile have drifted apart, which catches "forgot to commit the lockfile" mistakes immediately.
Coding challenge
Write a minimal but valid package.json for a small Express API named "task-api" at version 1.0.0, with express as a normal dependency (caret range) and jest as a dev dependency, plus "start" and "test" scripts.
// Write your solution here
Answer every question below to mark this chapter as complete.
Quiz Yourself
Score: 0/51. What does the caret in `"^4.19.2"` allow npm to install?
2. What distinguishes `~16.4.5` from `^16.4.5`?
3. Why should a package like Express go in `dependencies` rather than `devDependencies`?
4. What problem does committing package-lock.json solve?
5. Why does `npm ci` behave differently from `npm install` in a CI pipeline?