npm, package.json, scripts and semantic versioning
Read a package.json with confidence, know what ^ and ~ allow, and use the lockfile and npm ci so installs match everywhere.
Before you write a line of application code on a Node.js team, you will install dependencies, run scripts and probably fix a broken install. Interviewers ask about package.json because it shows whether you understand how a project is assembled. These are the parts that matter.
Reading package.json
{
"name": "orders-api",
"version": "1.4.2",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"lint": "eslint src"
},
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.1.0",
"eslint": "^8.57.0"
}
}dependencies are needed to run the app in production. devDependencies are needed only to build or test it. npm install express adds to the first; npm install --save-dev jest adds to the second. A production install with npm ci --omit=dev skips the dev tools.
Scripts
npm run dev runs the dev script. npm start and npm test are shortcuts that do not need run. Scripts can call tools from node_modules/.bin by name, which is why jest works without a path. A script named pretest runs automatically before test, and posttest after. npx eslint src runs a package without adding a script.
What ^ and ~ allow
Semantic versioning is MAJOR.MINOR.PATCH. Major means breaking changes, minor means new features, patch means fixes.
^4.19.2allows anything from 4.19.2 up to but not including 5.0.0.~4.19.2allows 4.19.2 up to but not including 4.20.0.4.19.2with no symbol pins that exact version.- Below 1.0.0 the caret is stricter:
^0.3.1allows 0.3.x only, because early versions treat minor bumps as breaking.
Worked example: a package.json says ^2.5.0 and the registry has 2.5.3, 2.9.0 and 3.0.1. A fresh install picks 2.9.0. The lockfile is what stops that from changing under you next week.
package-lock.json records the exact version of every package and sub-package that was installed. Commit it. npm install may update it; npm ci deletes node_modules and installs exactly what the lockfile says, failing if the two files disagree. Use npm ci in CI pipelines and on servers so every machine gets identical code. npm outdated lists packages with newer versions and npm audit lists known vulnerabilities.
Common mistakes and a practice habit
- Committing
node_modules. Add it to.gitignore. - Deleting the lockfile to fix an error. This usually hides the real problem and changes versions for everyone.
- Installing project tools globally, so the next developer's machine behaves differently.
- Putting a test runner in
dependencies, which bloats the production image.
Practice habit: create an empty project with npm init -y, add one dependency and one dev dependency, write a test script, then open the lockfile and find the exact version that was resolved. Repeat with a tilde range and compare.
More in Web and app