dependencies vs devDependencies
Not every package your project uses belongs in the same bucket. npm distinguishes four dependency types, and putting a package in the wrong one causes real bugs — a missing runtime module in production, or a bloated install that ships your test framework to users. This page makes the distinction precise.
The four types at a glance
Type | Holds | Installed for consumers? |
|---|---|---|
| Code that runs in production | Yes |
| Build, test, lint, type tooling | No |
| A host you plug into (e.g. React for a React plugin) | No — they must provide it |
| Nice-to-have; install may fail safely | Yes, but failure is tolerated |
The deciding question
There is one test that resolves almost every case:
Package | Bucket | Why |
|---|---|---|
|
| The server imports it at runtime |
|
| Loaded when the app boots |
|
| Only runs during testing |
|
| Compiles away before deploy |
|
| Developer tooling only |
|
| Types vanish at compile time |
How npm chooses where to save
npm install express # → dependencies (default) npm install -D typescript # → devDependencies (--save-dev) npm install -O fsevents # → optionalDependencies (--save-optional) npm install --save-peer react # → peerDependencies (npm 7+)
Resulting package.json
{
"dependencies": { "express": "^4.19.2" },
"devDependencies": { "typescript": "^5.4.0" }
}Why the split matters in production
When you deploy, you install runtime deps only — skipping devDependencies makes the install smaller, faster, and reduces attack surface:
npm ci --omit=dev # installs ONLY dependencies, not devDependencies
Peer dependencies
A peerDependency says: "I need package X, but the host application should own the single shared copy." Plugins use this so there is exactly one instance of the host (one React, one ESLint) — never two conflicting versions:
A React component library
{
"peerDependencies": { "react": ">=18" },
"devDependencies": { "react": "^18.2.0" }
}Optional dependencies
An optionalDependency is attempted, but if it fails to install (e.g. a native module that does not build on this OS), npm continues anyway. Your code must handle its absence:
let fsevents
try {
fsevents = require('fsevents') // macOS-only native module
} catch {
fsevents = null // fall back gracefully elsewhere
}