What is the difference between dependencies and devDependencies, and why do we need package-lock.json?
dependencies are packages your app needs to run in production; devDependencies are only needed while developing, testing or building. package-lock.json records the exact version of every installed package so every machine and deployment gets the same dependency tree.
Explain it simply
Think of building a house. The bricks, cement and wires stay in the house forever: those are dependencies. The scaffolding, measuring tape and cement mixer are needed only while building, and they leave when the house is ready: those are devDependencies.
package.json is your shopping list ("cement, about this brand, version 2 or newer"). package-lock.json is the actual bill from the shop: exactly which product, which batch and from where. If a friend wants to build the same house, they use the bill, not the rough list, so they get exactly the same materials.
Example
npm install express # added to "dependencies"
npm install -D jest eslint # added to "devDependencies"{
"name": "fest-registration",
"version": "1.0.0",
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"eslint": "^9.0.0",
"jest": "^30.0.0"
}
}The ^ in ^5.1.0 means "any 5.x.x version that is 5.1.0 or newer". So two people running npm install a month apart could get different versions without a lock file.
How it works
npm installinstalls both groups and updatespackage-lock.jsonif needed.npm install --omit=devinstalls onlydependencies. Production images use this to stay small.npm ciinstalls exactly whatpackage-lock.jsonsays, deletes any existingnode_modulesfirst, and fails ifpackage.jsonand the lock file do not match. It is the right command for CI and deployments.- The lock file stores the exact version, download URL and integrity hash of every package, including the dependencies of your dependencies.
Version ranges, quickly:
| Range | Allows |
|---|---|
^1.4.2 | 1.4.2 up to, not including, 2.0.0 |
~1.4.2 | 1.4.2 up to, not including, 1.5.0 |
1.4.2 | exactly 1.4.2 |
Real-world use
A team commits package-lock.json so that the code that passed tests in CI is built with the same packages as the code deployed to production. Build tools such as TypeScript, Vite, test runners and linters go in devDependencies. Express, a database driver or a date library go in dependencies.
Common mistakes
- Adding
package-lock.jsonto.gitignore. For apps, always commit it. - Putting a runtime package in devDependencies. It works locally, then crashes in production with "Cannot find module".
- Using
npm installin CI instead ofnpm ci, which can quietly change versions. - Deleting the lock file to "fix" an error, which upgrades many packages at once.
What to say in the interview
"dependencies are packages the app needs at runtime, like Express or a database driver. devDependencies are only for development and builds, like Jest, ESLint or TypeScript, and they are skipped with npm install --omit=dev. package.json usually stores version ranges such as caret ranges, so package-lock.json records the exact resolved version and integrity hash of every package in the tree. I commit the lock file and use npm ci in CI and deployments so every environment gets identical dependencies."
Follow-up questions
- What is the difference between
npm installandnpm ci? - What do
^and~mean in a version number? - What are peerDependencies?
Go deeper
Read these once the basics above feel easy.
peerDependencies say "I need this package, but the app should install it". A React component library lists react as a peer dependency so the app does not end up with two copies of React. Since npm 7, peer dependencies are installed automatically, and conflicts show up as ERESOLVE errors. For security, npm audit checks the locked versions against known vulnerabilities, and the integrity hashes in the lock file make sure a downloaded package was not tampered with.