What are peer dependencies?

April 5, 2026
web
npm

This is a short explanation of what peer dependencies are, in the context of NodeJS package managers.

Chances are if you’ve ever run an npm install, yarn install or pnpm install, you’d see some kind of message regarding “peer dependencies”. Or if you’ve looked at some open source library’s source code (or even a big project you’ve worked on), you might see something like the below:

"peerDependencies": {
 "react": "17.0.1"
}

If you’re like me, you usually just run the install and gloss over any messages regarding these “peer dependencies”, unless it gives you some kind of error that you have to resolve (usually one like the below) and then you’re left scratching your head, googling some stuff and then just running some random commands you don’t fully understand to get your install working as quickly as possible.

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! Found: @angular-devkit/build-angular@0.1102.5
npm ERR! node_modules/@angular-devkit/build-angular
npm ERR!   dev @angular-devkit/build-angular@"~0.1102.9" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! dev @angular-devkit/build-angular@"~0.1102.9" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: @angular/localize@11.2.10
npm ERR! node_modules/@angular/localize
npm ERR!   peerOptional @angular/localize@"^11.0.0 || ^11.2.0-next" from @angular-devkit/build-angular@0.1102.9
npm ERR!   node_modules/@angular-devkit/build-angular
npm ERR!     dev @angular-devkit/build-angular@"~0.1102.9" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

But what are peer dependencies exactly and why do they exist?

The Structure of npm Dependencies

As you probably know, most npm dependencies generally come as part of two types, besides peer dependencies; the standard dependencies object, which specifies your project’s main dependencies it requires to work, as well as the devDependencies object, which specifies the dependencies your project requires for development or building purposes. Both of these end up in the following structure in your node_modules directory when installing:

├── example-dependency-1@2.12.0
└─┬ example-dependency-2@1.2.3
  └── dependency-of-example-dependency-2@1.9.9
└─┬ example-dependency-3@1.2.1
  └── dependency-of-example-dependency-3@1.9.1

That is, every dependency is installed in its own folder inside node_modules, and its sub-dependencies are then nested inside that dependency’s folder as well. This is fine because every dependency works independently of each other and their sub-dependencies (and the package versions that each dependency requires) are isolated from each other, and no conflicts occur.

However there is a third use case wherein peer dependencies fit in: plugins. Plugins are usually libraries or modules that attach on to a “host” or main library to extend its functionality. As you’ve probably realised - this means that the plugin needs to specify which version of the “host” or main library/package it depends on. For example, if I develop a Vite plugin that works with Vite v7, that plugin will generally only work with Vite v7, and not Vite v6, v5, v4, or v8.

However, there again comes in an issue of specifically which version of Vite v7 that plugin requires - is it v7.0.0 or v7.1.1, or will it work for all versions in v7.1.x etc. This is where semver notation is also important. If we use the generally known way of specifying dependencies in the plugin’s package.json file i.e.: in the dependencies object for our example Vite plugin that uses Vite v7.1.1, like below:

{
  "dependencies": {
    "vite": "7.1.1",
  }
}

If we then use this plugin in a project that uses Vite as it’s bundler, we end up with the following dependencies object in the overall project:

{
  "dependencies": {
    "vite": "7.1.1",
    "example-vite-plugin": "1.0.0"
  }
}

This would then cause Vite to be installed twice in the node_modules folder, once for the main Vite package, and then another for the Vite example plugin, like below:

├── vite@7.1.1
└─┬ example-vite-plugin@1.0.0
  └── vite@7.1.1

As you can see, this causes duplication of dependencies, taking up unnecessary storage space, since you already know the example plugin requires Vite v7.1.1 to be there before installing the example plugin, as well as another major problem: what if the overall project upgraded to Vite v7.1.2, but the example plugin only works and integrates with Vite v7.1.1? Your bundling process would end up breaking and now you would need to wait for the example plugin to be upgraded to compatible with Vite v7.1.2, and released, before you can unblock your project bundling or upgrade to Vite v7.1.2.

The Purpose of Peer Dependencies

If we consider the above problems, the easiest way to resolve them would be to specify that this plugin expects Vite v7.1.1 to be already provided by the project installing the plugin, instead of installing it as a sub-dependency, so that you ensure the correct and compatible version of Vite is already being made use of by the host project, and the common dependency (Vite v7.1.1) is shared and not installed twice. This is the purpose of peer dependencies and why they exist within npm.

To solve our example problem above, if we specify a peerDependencies object inside the package.json file of our example-vite-plugin package like below:

"peerDependencies": {
 "vite": "7.1.1"
}

This will ensure that the “host” project already has Vite v7.1.1 installed otherwise it will throw an error. To modify Dominic Denicola’s succinctly put example for our needs (his article is linked in the references section): “I only work when plugged in to version 7.1.1 of my host package (which is Vite 7.1.1 in this case), so if you install me, be sure that it’s alongside a compatible host (Vite 7.1.1)”.

Some Gotchas

As we mentioned above, semver for peer dependencies is quite important - if your plugin requires a specific version of the host library (like in our example - Vite v7.1.1), it will very likely cause errors and problems as this is too restrictive. It is better to specify peer dependencies within ranges of version (and that your plugin is compatible with the host’s version within those ranges) i.e. using our example: vite: 7.1.x, or vite: ^7.1.1 etc.

Another thing to note is that npm versions 1, 2 and 7 will automatically install peer dependencies when they are specified, but versions 3 to 6 will not (only a warning will be printed) and you would have to install them manually.

—legacy-peer-deps

You most likely have seen (and used) this flag to resolve npm install problems before. Congrats - this is directly related to peer dependencies. In v7 of npm, if there’s a peer dependency conflict error (i.e. your app uses Vite v6, but another package’s peer dependencies specifies v7), it will throw a hard error and stop the entire npm install. You have to either manually resolve the conflict by upgrading or downgrading packages to ensure compatiblity - or you can use the --legacy-peer-deps or the --force flags when installing. They do the following:

  • --legacy-peer-deps — skips peer dependency resolution entirely, and behaves like npm 6 (just gives a warning)
  • --force — powers through conflicts, but can result in a broken install

Conclusion

I hope this serves as good and understandable explanation of what peer dependencies are. As always, feel free to provide any corrections.

References: