DEV Community

Cover image for All npm Dependency Types Explained: dependencies, devDependencies, peerDependencies, and More
Muhammad Hamid Raza
Muhammad Hamid Raza

Posted on

All npm Dependency Types Explained: dependencies, devDependencies, peerDependencies, and More

Ever stared at a package.json file and wondered why some packages live under dependencies and others under devDependencies? šŸ¤” And then you spotted peerDependencies hiding at the bottom like a ghost you didn't know existed?

You are not alone. This trips up beginners and even mid-level developers who never stopped to think about it deeply.

Here is the truth: putting a package in the wrong dependency type does not just feel wrong — it can bloat your production build, break someone else's project, or cause weird version conflicts that are painful to debug.

So let's fix that today. By the end of this post, you will know exactly where every package belongs and why it matters.


What Is a Dependency in npm?

A dependency is any external package your project needs to do its job.

Think of it like ingredients in a recipe. Some ingredients go into the final dish (the user eats them). Some are just tools you used while cooking — like a mixer or a timer — that the guest never sees.

npm uses your package.json file to track all of these ingredients in different labeled boxes, depending on when and where they are needed.


All Dependency Types in npm

Here is the full picture — every field that can appear in your package.json:

Type Key in package.json When It Is Used
Runtime dependency dependencies In production, always
Dev-only dependency devDependencies Only during development
Peer dependency peerDependencies Shared with the host project
Peer metadata peerDependenciesMeta Marks a peer dep as optional
Optional dependency optionalDependencies Nice to have, not required
Bundled dependency bundledDependencies Shipped inside your package

And separately, npm gives you install-time save flags that control which category a package lands in. We will cover those too.

Let's break each one down properly.


1. dependencies — The Production Essentials

These are packages your app needs to run in production. Without them, your app breaks.

"dependencies": {
  "express": "^4.18.2",
  "axios": "^1.6.0",
  "react": "^18.2.0"
}
Enter fullscreen mode Exit fullscreen mode

Real example: If you are building a React app, React itself goes here. If you are building an API with Express, Express goes here. Your users and servers need these packages to exist at runtime.

Install command:

npm install express
# or
npm install --save express
Enter fullscreen mode Exit fullscreen mode

āœ… Rule: If removing this package makes your app crash or fail in production, it belongs in dependencies.


2. devDependencies — The Dev-Time Helpers

These are packages you only need while building, testing, or writing code. They never go to production.

"devDependencies": {
  "eslint": "^8.57.0",
  "typescript": "^5.4.2",
  "jest": "^29.7.0",
  "vite": "^5.2.0"
}
Enter fullscreen mode Exit fullscreen mode

Real example: ESLint checks your code style. Jest runs your tests. Vite builds your project. None of these run when a user opens your app in the browser. They are just tools you use at your desk.

Install command:

npm install --save-dev eslint
# or
npm install -D eslint
Enter fullscreen mode Exit fullscreen mode

āœ… Rule: If the package is only used for linting, testing, compiling, or bundling — it belongs in devDependencies.

Why does this matter?

When someone deploys your app using npm install --production (or sets NODE_ENV=production), npm skips devDependencies. This keeps your production environment lean and fast. Shipping test runners to production is a waste of space and a security risk.


3. peerDependencies — The Sharing Agreement

This one is mostly for library and plugin authors, not app developers.

When you build a library (say, a custom React component library), you expect the project using your library to already have React installed. You do not want to bundle your own copy of React — that would cause version conflicts and bloat.

So instead of adding React to your dependencies, you declare it as a peer dependency.

"peerDependencies": {
  "react": ">=17.0.0",
  "react-dom": ">=17.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Real example: A package like react-query does not install its own React. It says: "Hey, I expect you to already have React. Make sure the version is compatible."

Install command:

Peer dependencies are NOT automatically installed by npm (in npm v7+, npm will warn you if they are missing but tries to install them). In older versions, you had to install them yourself.

āœ… Rule: If you are writing a package or plugin that relies on the host project having a dependency, use peerDependencies.


4. peerDependenciesMeta — The Peer Dependency Modifier

This one is small but important. It does not stand alone — it works alongside peerDependencies to mark specific peer deps as optional.

By default, if a peer dependency is missing, npm throws a warning (or error). But sometimes you want to say "React Native is a peer dep, but only if the user is building a React Native project." That is where peerDependenciesMeta comes in.

"peerDependencies": {
  "react": ">=17.0.0",
  "react-native": ">=0.70.0"
},
"peerDependenciesMeta": {
  "react-native": {
    "optional": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Real example: A UI library that supports both web and React Native can list react-native as an optional peer dependency. Web-only users won't get a warning about a missing package they don't need.

āœ… Rule: Use peerDependenciesMeta when you have peer dependencies that are only relevant in certain environments. It silences unnecessary warnings for users who don't need them.


5. optionalDependencies — The Nice-to-Have

These are packages your project can use if available, but your app will not crash if they fail to install.

"optionalDependencies": {
  "fsevents": "^2.3.3"
}
Enter fullscreen mode Exit fullscreen mode

Real example: fsevents is a native file-watching module that only works on macOS. Some packages list it as optional because it improves performance on Mac, but the package still works fine on Windows and Linux without it.

Install command:

npm install --save-optional fsevents
Enter fullscreen mode Exit fullscreen mode

āœ… Rule: If the package is a performance boost or platform-specific extra — not a core requirement — use optionalDependencies. But make sure your code handles the case where it is absent.


6. bundledDependencies — The Package-in-a-Package

This one is rare. Also written as bundleDependencies (both work).

It is a list of package names that will be physically bundled inside your npm package when you publish it with npm pack or npm publish. Instead of letting the user's npm fetch them from the registry, they come bundled with your package.

"bundledDependencies": [
  "some-internal-util"
]
Enter fullscreen mode Exit fullscreen mode

Real example: If you have a private internal package or a fork of a package you modified, you can bundle it directly so users always get the exact version you tested with — without depending on the registry.

āœ… Rule: Use bundledDependencies only when you need to ship specific packages alongside your own — like private utilities or heavily patched forks. Most developers never need this.


7. npm Install Save Flags — How Each Type Gets Added

Every dependency type has a matching npm install flag that controls where the package lands in package.json. This is the part most tutorials skip.

Here is the complete reference:

Flag Shorthand Where It Saves Example
--save -S dependencies npm install axios --save
--save-dev -D devDependencies npm install jest -D
--save-optional -O optionalDependencies npm install fsevents -O
--save-peer (none) peerDependencies npm install react --save-peer
--save-bundle -B bundledDependencies npm install util -B
--save-exact -E Saves exact version (modifier) npm install lodash -E
--no-save (none) Does not save to package.json npm install chalk --no-save

Let's talk about each one briefly.

--save or -S (Default since npm v5)

This saves the package to dependencies. Before npm version 5, you had to write --save explicitly or the package would install but not appear in package.json. Since npm v5, running npm install package-name does the same thing automatically.

npm install express        # same as:
npm install express --save
npm install express -S
Enter fullscreen mode Exit fullscreen mode

All three do the exact same thing today. You will still see --save in older tutorials — it is not wrong, just no longer necessary.

--save-dev or -D

Saves to devDependencies. Use this for linters, test runners, type checkers, and build tools.

npm install typescript --save-dev
npm install typescript -D
Enter fullscreen mode Exit fullscreen mode

--save-optional or -O

Saves to optionalDependencies. The package is installed if possible but skipped without an error if it fails.

npm install fsevents --save-optional
npm install fsevents -O
Enter fullscreen mode Exit fullscreen mode

--save-peer (npm v7+)

Saves to peerDependencies. This flag was added in npm v7. Before that, you had to manually edit package.json to add peer dependencies.

npm install react --save-peer
Enter fullscreen mode Exit fullscreen mode

⚔ Note: This is a relatively newer flag. If you are using an older npm version, you may need to add peerDependencies manually.

--save-bundle or -B

Saves to bundledDependencies. The package name is added to the array (not as a key-value pair like others).

npm install some-util --save-bundle
npm install some-util -B
Enter fullscreen mode Exit fullscreen mode

--save-exact or -E

This is a modifier, not a category. It does not change which section the package goes into — it just removes the ^ version prefix and saves the exact version number instead.

npm install lodash -E          # saves "lodash": "4.17.21" instead of "^4.17.21"
npm install lodash -D -E       # exact version saved to devDependencies
Enter fullscreen mode Exit fullscreen mode

Use this when you need pinned, reproducible installs and do not want any automatic minor or patch updates.

--no-save

Installs the package into node_modules but does not write anything to package.json. Useful for quick one-off testing of a package without committing it to your project.

npm install some-package --no-save
Enter fullscreen mode Exit fullscreen mode

Quick Visual Summary

package.json dependency fields:

Your project (package.json)
│
ā”œā”€ā”€ dependencies            → Runs in production. User needs it. āœ…
ā”œā”€ā”€ devDependencies         → Dev-only. Never ships to production. šŸ”§
ā”œā”€ā”€ peerDependencies        → For library authors. "Bring your own." šŸ“¦
ā”œā”€ā”€ peerDependenciesMeta    → Marks specific peer deps as optional. šŸ·ļø
ā”œā”€ā”€ optionalDependencies    → Nice to have. Works fine without it. 🤷
└── bundledDependencies     → Ships physically inside your package. šŸ“
Enter fullscreen mode Exit fullscreen mode

npm install save flags:

npm install <pkg>              → dependencies (default, same as --save)
npm install <pkg> --save       → dependencies  (-S)
npm install <pkg> --save-dev   → devDependencies  (-D)
npm install <pkg> --save-optional → optionalDependencies  (-O)
npm install <pkg> --save-peer  → peerDependencies  (npm v7+)
npm install <pkg> --save-bundle → bundledDependencies  (-B)
npm install <pkg> --save-exact  → exact version pin, any category  (-E)
npm install <pkg> --no-save    → installs but does NOT save anywhere
Enter fullscreen mode Exit fullscreen mode

Why This Actually Matters (Real Impact)

1. Smaller production builds

If your dependencies list contains testing libraries, linters, or build tools, your deployment gets unnecessarily heavy. A slim production node_modules loads faster and reduces attack surface.

2. No version conflicts in libraries

Wrong use of dependencies instead of peerDependencies in a library means two versions of React might end up in the same project. That causes hooks to break and warnings to fire — and debugging it is a nightmare.

3. Cleaner CI/CD pipelines

When CI knows which packages are for production and which are for dev, it can run optimized installs. npm ci --production in a deploy step skips all dev tools, saving time.

4. Better collaboration

When a new developer clones your project and runs npm install, they get everything they need — no more, no less — because the categories are correct.


Common Mistakes Developers Make

āŒ Installing everything under dependencies

This is the most common mistake. Developers just run npm install package-name and move on, without thinking about whether it is a dev or runtime package.

Fix: Before installing, ask yourself: "Does this package need to exist when my app is running for users?" If yes → dependencies. If no → devDependencies.


āŒ Using dependencies instead of peerDependencies in a library

If you are building a React component library and you add React to your dependencies, every project that installs your library might end up with two React instances. Two React instances = broken hooks and lots of confusion.

Fix: Always use peerDependencies for shared host packages when building libraries or plugins.


āŒ Ignoring optionalDependencies error handling

If a package is optional, your code must handle the case where it does not exist. Many developers declare something as optional but then require() it without a try-catch, causing crashes.

// Bad
const fsevents = require('fsevents');

// Good
let fsevents;
try {
  fsevents = require('fsevents');
} catch {
  // Not available, continue without it
}
Enter fullscreen mode Exit fullscreen mode

āŒ Confusing peerDependencies with devDependencies

When writing a library, you still need React locally to build and test. So you add it to both devDependencies (for local dev) and peerDependencies (to declare what the host needs). Many developers only add it to one — and then wonder why their build fails or why users get version conflicts.


Do's & Don'ts

Do āœ… Don't āŒ
Use -D for linters, test runners, bundlers Don't ship ESLint or Jest to production
Use peerDependencies in npm libraries Don't bundle React inside your component library
Handle missing optionalDependencies in code Don't assume optional packages always install
Audit your dependencies before deploying Don't just run npm install x blindly every time
Keep bundledDependencies for rare edge cases Don't use it as a lazy substitute for proper publishing

Conclusion

Understanding dependency types in npm is one of those small things that makes a huge difference as your projects grow. šŸš€

The short version:

package.json fields:

  • dependencies → Your app needs this at runtime. Always.
  • devDependencies → Only needed while you are coding. Never ships.
  • peerDependencies → For library authors. Let the host project provide it.
  • peerDependenciesMeta → Marks a peer dep as optional to silence warnings.
  • optionalDependencies → Nice to have. Handle gracefully if missing.
  • bundledDependencies → Rare. Packages shipped inside your package.

npm install flags:

  • --save / -S → Saves to dependencies (default since npm v5).
  • --save-dev / -D → Saves to devDependencies.
  • --save-optional / -O → Saves to optionalDependencies.
  • --save-peer → Saves to peerDependencies (npm v7+).
  • --save-bundle / -B → Saves to bundledDependencies.
  • --save-exact / -E → Pins exact version. Combines with any flag.
  • --no-save → Installs without touching package.json.

Getting this right keeps your production builds lean, your libraries conflict-free, and your package.json clean and professional.

If you found this helpful, share it with a teammate who is still dumping everything into dependencies 😊 — it might save them a frustrating debug session.

For more posts like this, head over to hamidrazadev.com where I write about frontend development, tools, and real-world developer problems.

Top comments (1)

Collapse
 
nazar_boyko profile image
Nazar Boyko

One thing worth folding in, since this is the page people will bookmark. The --production flag you mention is on its way out, and npm now steers you toward --omit=dev for the same job (npm ci --omit=dev in a deploy step). Same idea you're describing, just the spelling that won't print a deprecation notice on newer npm.