Using npm Workspaces to Hoist Dependencies in a Monorepo
Learn how to set up npm workspaces, hoist dependencies, verify shared copies, and handle version conflicts in a monorepo.
21 Oct 2025, 08:41 UTC

The problem: duplicated installs in a growing monorepo
When a repository holds several related packages, each with its own package.json, running npm install in every folder creates separate node_modules trees. This wastes disk space, slows down CI, and makes it easy for different packages to drift onto different versions of the same dependency.
Thesis: npm workspaces hoist shared dependencies to a single root node_modules, giving a predictable, space‑efficient layout while still letting each workspace declare its own version ranges.
Enabling workspaces
- At the repository root, add a
"workspaces"array topackage.jsonthat lists the relative paths of each workspace. A common pattern is to use a glob:
{
"private": true,
"workspaces": ["packages/*"]
}
- Run
npm installfrom the root directory. npm creates a singlenode_modulesfolder at the root and symlinks each workspace into it. - Each workspace can still have its own
package.jsonwith dependencies, scripts, and version.
Worked example: two libraries sharing a utility
Assume the following layout:
my-monorepo/ ├─ package.json # root, with workspaces ├─ packages/ │ ├─ lib-a/ │ │ └─ package.json │ └─ lib-b/ │ └─ package.json └─ node_modules/ # created after install
Root package.json (as shown above).
packages/lib-a/package.json:
{
"name": "lib-a",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}
packages/lib-b/package.json:
{
"name": "lib-b",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21",
"lib-a": "1.0.0"
}
}
After running npm install at the root (requires npm ≥ 7):
- A single
node_modulesappears at the repository root. - Inside it,
lodashis installed once atnode_modules/lodash. - Both
lib-aandlib-bare symlinked intonode_modulesand canrequire('lodash')orimport lodash from 'lodash'without extra linking.
Verifying the hoist
You can confirm that both workspaces see the same copy:
# From the repository root
cd packages/lib-a
node -e "console.log(require.resolve('lodash'))"
cd ../lib-b
node -e "console.log(require.resolve('lodash'))"
Both commands should print the same absolute path, e.g., /path/to/my-monorepo/node_modules/lodash/package.json.
Alternatively, run:
npm ls lodash
which shows a single entry under the root node_modules.
Trade‑off and limitation: version hoisting can hide mismatches
npm hoists a dependency to the version that satisfies the most dependents. When workspace version ranges conflict, npm may nest a duplicate copy for the workspace that needs a different version. This means a workspace can receive a version outside its declared range, and the resulting error may only appear at runtime.
Practical check: after npm install, run npm ls <package> from each workspace directory. If you notice a workspace receiving a version outside its declared range, you can:
- Add an explicit
"overrides"field in the rootpackage.jsonto force the desired version, or - Declare the dependency as a
devDependencyin the workspace that needs the specific version and avoid hoisting it.
Actionable closing
Start small: add a workspaces array to your root package.json, run npm install from the root, and verify a single node_modules tree with npm ls. Keep an eye on version ranges; use npm ls or require.resolve to confirm each workspace gets the expected copy. When a conflict appears, employ overrides or adjust the workspace’s dependencies rather than abandoning workspaces altogether.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.