NodeJSdependencies vs devDependencies

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?

dependencies

Code that runs in production

Yes

devDependencies

Build, test, lint, type tooling

No

peerDependencies

A host you plug into (e.g. React for a React plugin)

No — they must provide it

optionalDependencies

Nice-to-have; install may fail safely

Yes, but failure is tolerated

The deciding question

There is one test that resolves almost every case:

Does the running application import it?
If the code that executes in production does `require`/`import` it — even indirectly — it is a **dependency**. If it is only used by a command you run *before* deploying (compiling, bundling, testing, linting) and the shipped code never imports it, it is a **devDependency**. When unsure, ask: "if this package vanished in production, would my app crash?" Yes → dependency.

Package

Bucket

Why

express

dependencies

The server imports it at runtime

dotenv

dependencies

Loaded when the app boots

jest / vitest

devDependencies

Only runs during testing

typescript

devDependencies

Compiles away before deploy

eslint / prettier

devDependencies

Developer tooling only

@types/node

devDependencies

Types vanish at compile time

How npm chooses where to save

Bash
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

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:

Bash
npm ci --omit=dev      # installs ONLY dependencies, not devDependencies
A runtime import in devDependencies = production crash
This is the classic mistake: a package your server actually imports is saved under `devDependencies`. It works locally (where you install everything) but, after `npm ci --omit=dev`, the module is absent in production and the app dies with `Cannot find module`. If your running code imports it, it belongs in `dependencies` — no exceptions.
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

JSON
{
  "peerDependencies": { "react": ">=18" },
  "devDependencies":  { "react": "^18.2.0" }
}
Why list React in BOTH
The `peerDependency` declares "the host must supply React 18+". But to develop and test the library *yourself*, you still need a real React installed — hence it also appears in `devDependencies`. Consumers get React from their own app; you get it from devDeps. Since npm 7, unmet peers cause install errors (see [Installing Packages](/nodejs/installing-packages)).
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:

JS
let fsevents
try {
  fsevents = require('fsevents')   // macOS-only native module
} catch {
  fsevents = null                   // fall back gracefully elsewhere
}
Next
Those `^` and `~` symbols in front of versions are not decoration — they define exactly which updates are allowed: [Semantic Versioning](/nodejs/semantic-versioning).