Difference between dependencies, devDependencies and peerDependencies

Farhan CK

By Farhan CK

on May 14, 2024

In a JavaScript project, understanding the distinctions between dependencies, devDependencies, and peerDependencies is crucial for effective package management. Each plays a distinct role in shaping how a project is built and distributed. In this blog, we'll explore these terms and their differences.

dependencies

The packages that are really needed for a project to function should be listed under dependencies. These packages are always installed within that project. If the project is also a package, then these dependencies will also get installed in the host project that uses this package. Below are some common examples of what might go under dependencies.

1 "dependencies": {
2   "dayjs": "1.11.1",
3   "immer": "^10.0.2",
4   "ramda": "^0.29.0",
5   "react": "^18.2.0"
6 }

It's important to understand that above packages does not always have to be under dependencies. For example if dayjs is needed only for development, deployment or testing purposes, It should not be listed under dependencies but rather in the devDependencies section, because dependencies will be bundled with the main code, whereas devDependencies will not. So adding dependencies that are only used in development purposes under dependencies is unnecessary and will increase the bundle size, affecting the performance of the application.

To add a package to dependencies section, simply run:

1yarn add package-name

If we are shipping a package, all our dependencies are installed in the root of host projects node_modules. However, an exception occurs if the host project already has the same dependency but with a different version. In this scenario, that specific conflicting dependency will be installed within the node_modules of our package to avoid conflicts between versions.

To explain it bit more lets say we have dayjs@1.0.0 as one of our dependency but the host project uses 2.0.0. Then both version will be installed, 2.0.0 would be the in root node_modules and 1.0.0 in our package's node_modules. This way our package can continue to use 1.0.0 while host uses 2.0.0. The folder structure of that host will look something like below.

1host-project-app
2├── src
3│   └── index.js
4├── node_modules
5│   └── react
6│   └── dayjs
7│   └── my-awesome-lib
8│     └── node_modules
9│       └── dayjs
10├── package.json
11└── README.md

devDependencies

Packages listed in devDependencies are used specifically for development purposes. Any dependencies that does not go into our actual code is listed here and not under dependencies. These dependencies won't be installed in a production environment or in a host project if our project acts as a package. Here are a few examples of what might belong in devDependencies.

1 "devDependencies": {
2    "@babel/core": "^7.16.5",
3    "eslint": "^8.41.0",
4    "husky": "^7.0.4",
5    "jest": "27.5.1",
6    "prettier": "^2.8.8"
7 }

Just like in dependencies, These packages may not always be in devDependencies. For example if we are building a package that enhances Jest tests capabilities, then we should place jest under dependencies. Otherwise our host project will break, because jest will not be installed in the host project.

To add a package to devDependencies section, simply run:

1yarn add -D package-name

peerDependencies

peerDependencies are needed only if we are building a package. It allows host to install any desired version unless we specify otherwise.

If we are using yarn as the package manager then peerDependencies are not installed automatically. We have to install manually, even in our own package. But there is a slight difference if you are using npm. Up to version 6 npm does not install peerDependencies automatically. However, this changes from version 7 onwards, as it will install peerDependencies automatically. So if we are using yarn or npm<=v6 and we have for example Storybook or Jest tests in our package project, then we have to install peerDependencies as devDependencies(not as dependencies which defeats the purpose) as well.

1 "devDependencies": {
2    "@babel/core": "^7.16.5",
3    "eslint": "^8.41.0",
4    "husky": "^7.0.4",
5    "jest": "27.5.1",
6    "prettier": "^2.8.8",
7    "dayjs": "1.11.1",
8 }
9 "peerDependencies": {
10    "dayjs": "latest"
11 }

You might wonder about its purpose if manual installation is required. It serves as a means for our package project to specify essential dependencies required for the package to function properly. Simultaneously, it grants control to the host project to choose which versions to install.

To add a package to peerDependencies section, simply run:

1yarn add --peer package-name

Now the tricky part is determining whether a particular dependency should go under dependencies or peerDependencies. Unfortunately there is no clear answer here. But there are some questions we can ask ourself to narrow it down.

  1. If the specific version of the dependency is important for our package, that is if using a different version breaks our package, then definitely we want to place that package under dependencies.

    It is possible to place such dependencies under peerDependencies and specify supported versions like below, but it is not a good practice.

1 "peerDependencies": {
2    "dayjs": "1.2.0 || 1.5.1",
3 }
  1. If the dependency is a widely used package like react, better place it under peerDependencyand ensure that our package works with different version of that dependency. This way, the dependency installed in the host project can be reused by our package without installing a separate version.
  2. If we want to make changes to a dependency in a manner that impacts its usage within the host project, then place it under peerDependencies. Good example of this is, at neeto we have a package called neeto-commons-frontend which extracts numerous common functionalities utilized across various products. One such functionality is our error handling system, for which we use an Axios interceptor. To ensure the functionality of this interceptor, it's crucial to apply this interceptor to the same instance of Axios. To elaborate further, if we add Axios under dependencies in neeto-commons-frontend, but the host project uses a different Axios version, we will be making changes to the Axios instance that's in node_modules of neeto-commons-frontend and not of the host project, means the functionality will not work on the host project.

Another rationale behind utilizing peerDependencies is its substantial impact on reducing the bundle size, particularly when bundling our package. Now if we are using Rollup, placing certain packages under peerDependencies doesn't automatically exclude them from the bundle. We need to explicitly specify this in the Rollup configuration. This is achieved through the Rollup external configuration option, where we provide a list of peerDependencies to be excluded from the bundle. To simplify this process, rollup-plugin-peer-deps-external automates the inclusion of peerDependencies within the external configuration.

1import peerDepsExternal from "rollup-plugin-peer-deps-external";
2
3export default {
4  plugins: [
5    // Preferably set as first plugin.
6    peerDepsExternal(),
7  ],
8};

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.