Managing Dependencies
npm separates concerns by dependency type. Rather than dumping every package into a single bucket, package.json distinguishes between packages your application needs at runtime, tools you only need during development, and packages that the consumer of your library must supply themselves. Getting these categories right keeps production installs lean and avoids dependency conflicts downstream.
dependencies — runtime packages
Section titled “dependencies — runtime packages”dependencies lists packages that must be present for your application to run. When someone installs your package, npm installs these alongside it.
npm install expressThis adds express to the "dependencies" section of package.json:
{ "dependencies": { "express": "^4.18.2" }}devDependencies — development-only tools
Section titled “devDependencies — development-only tools”devDependencies contains packages used only while building, testing, or linting — they are never needed in production. npm skips them when NODE_ENV=production or when --omit=dev is passed.
npm install --save-dev eslint vitest typescript{ "devDependencies": { "eslint": "^8.57.0", "typescript": "^5.4.5", "vitest": "^1.6.0" }}peerDependencies — consumer-provided packages
Section titled “peerDependencies — consumer-provided packages”peerDependencies declares packages that the consuming project must install. This is the correct pattern for plugins, framework integrations, and shared libraries that should not be bundled separately in every package that uses them.
{ "name": "eslint-plugin-my-rules", "version": "1.0.0", "peerDependencies": { "eslint": ">=8.0.0" }}If a consumer installs eslint-plugin-my-rules without eslint present, npm prints a peer dependency warning. The plugin itself never includes its own copy of eslint — it uses the one in the consumer’s project.
Lockfiles
Section titled “Lockfiles”package-lock.json records the exact resolved version and integrity hash of every package (and its transitive dependencies) that was installed. This guarantees that every developer and every CI run gets identical trees.
A minimal lockfile entry looks like:
{ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTg==" }}Always commit package-lock.json to version control. Without it, npm install may resolve to newer patch or minor versions, silently breaking reproducible builds.
npm install vs npm ci
Section titled “npm install vs npm ci”These two commands serve different purposes:
# npm install — resolves and potentially updates the lockfilenpm install
# npm ci — installs exactly what is in the lockfile; fails if package.json# and package-lock.json are out of sync; always starts with a clean slatenpm cinpm install | npm ci | |
|---|---|---|
| Lockfile | May update | Must match — fails if out of sync |
| node_modules | Incremental | Deleted and rebuilt from scratch |
| Use case | Local development | CI/CD pipelines, Docker builds |
| Speed | Slower (resolves) | Faster (reads lockfile directly) |