npm dedupe, despite its name, doesn’t actually remove duplicate dependencies. Instead, it tries to flatten your node_modules directory, moving packages higher up the tree to satisfy multiple dependents with a single copy.

Let’s see it in action. Imagine you have a project with two direct dependencies: lodash version 4.17.21 and moment version 2.29.4.

package.json:

{
  "dependencies": {
    "lodash": "^4.17.21",
    "moment": "^2.29.4"
  }
}

After running npm install, your node_modules might look something like this:

node_modules/ lodash/ moment/

Now, let’s say you add another dependency, express, which itself depends on lodash but requests a slightly older version, say 4.17.10.

package.json:

{
  "dependencies": {
    "lodash": "^4.17.21",
    "moment": "^2.29.4",
    "express": "^4.18.2"
  }
}

After installing express and running npm install again, you might end up with:

node_modules/ lodash/ (version 4.17.21) moment/ express/ node_modules/ lodash/ (version 4.17.10)

This is a "phantom dependency" scenario. Your top-level lodash is 4.17.21, but express is installed inside its own node_modules folder, pointing to an older 4.17.10. This can lead to unexpected behavior because express might be using a different version of lodash than your application code.

When you run npm dedupe, npm walks through your node_modules tree. It looks for packages with the same name and compatible version ranges. If it finds multiple copies of a package, it attempts to hoist the highest compatible version to the root node_modules directory.

After npm dedupe, the structure might change to:

node_modules/ lodash/ (version 4.17.21) moment/ express/

Notice how express no longer has its own node_modules/lodash directory. Instead, express now resolves to the top-level lodash (4.17.21). This happens because 4.17.21 is a compatible version for any dependency that requested ^4.17.10. npm prioritizes satisfying dependencies with a single, top-level copy whenever possible.

The primary problem npm dedupe aims to solve is dependency hell, particularly the "phantom dependency" issue. When a nested dependency (like express in our example) relies on a specific version of a package (lodash), but that package isn’t directly installed at the root level, your application might inadvertently use the root-level version, leading to runtime errors. npm dedupe helps by consolidating these dependencies, ensuring that all parts of your application resolve to the same, compatible versions of shared packages. It also has a minor benefit of reducing disk space and potentially speeding up install times by avoiding redundant copies of packages.

However, it’s crucial to understand that npm dedupe does not guarantee that all duplicate dependencies are eliminated. If two dependencies require different, incompatible versions of the same package (e.g., packageA needs lodash@4.17.21 and packageB needs lodash@3.10.0), npm will install both versions. In such cases, npm dedupe will create a node_modules structure where each required version resides in its respective dependent’s node_modules folder. For instance, packageA would have node_modules/lodash (4.17.21) and packageB would have node_modules/lodash (3.10.0). The command’s goal is to satisfy as many dependents as possible with a single, top-level copy, not to force a single version where incompatible ones are explicitly requested.

A common misconception is that npm dedupe will magically fix version conflicts. It won’t. If your package-lock.json (or npm-shrinkwrap.json) specifies different, incompatible versions for the same package across your dependency tree, npm dedupe will respect those requirements and install multiple copies, each nested appropriately. The real fix for incompatible versions lies in updating your direct dependencies or the problematic transitive dependencies themselves to a point where they agree on a compatible version range.

The true power of npm dedupe lies in its ability to reconcile compatible version requests. When multiple dependencies can be satisfied by a single version of a package (e.g., both need lodash^4.17.0 and the highest available is 4.17.21), npm dedupe will pull that 4.17.21 up to the root node_modules and remove the nested copies. This makes your dependency tree more predictable and less prone to phantom dependency issues.

When you run npm dedupe, npm reads your package-lock.json to understand the intended dependency structure. It then examines the node_modules folder and attempts to rearrange it to match the most "flattened" state possible, prioritizing hoisting compatible versions to the top level. If a package is required by multiple dependents and a single version satisfies all their version ranges, that version will be moved to the root. If, however, two dependents require mutually exclusive versions (e.g., packageA needs react@17.x.x and packageB needs react@16.x.x), npm dedupe will not resolve this conflict; both versions will likely coexist, nested within their respective dependents.

The next challenge you’ll likely encounter is understanding how npm ci interacts with a deduplicated node_modules structure and its reliance on the package-lock.json for precise installations.

Want structured learning?

Take the full Npm course →