Managing Monorepos with npm Workspaces: Hoisting and Local Linking
Learn how to use npm Workspaces to manage monorepos, optimize dependency hoisting, and link local packages using a single root lockfile.
14 Aug 2025, 07:22 UTC

Solving Dependency Redundancy in Monorepos
Managing multiple related packages in a single repository often leads to duplicated node_modules folders, bloated disk usage, and the manual overhead of linking local packages for development. npm Workspaces solve this by consolidating dependency management into a single root lockfile and using symlinks to connect internal packages.
The primary takeaway is that npm Workspaces automate the npm link process and optimize installation by hoisting shared dependencies to the root directory, ensuring that all packages use the same version of a library unless explicitly specified otherwise.
Implementing a Workspace Configuration
To implement workspaces, you must define the location of your sub-packages in the root package.json. This tells npm to treat those directories as part of a single dependency graph.
Example Directory Structure
/my-project
├── package.json (Root)
├── node_modules/
└── packages/
├── shared-utils (Local Package)
└── web-app (Local Package)
Root Configuration
In the root package.json, add the workspaces field. This example assumes all packages live inside a packages/ folder:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}
Linking Local Packages
If web-app needs to use shared-utils, you do not install it via a registry. Instead, add it to the web-app/package.json using the exact version defined in the shared-utils/package.json`:
// packages/web-app/package.json
{
"name": "web-app",
"dependencies": {
"shared-utils": "1.0.0"
}
}
When you run npm install at the root, npm creates a symbolic link (symlink) in the root node_modules that points to the packages/shared-utils directory. The web-app can now call require('shared-utils') or import from 'shared-utils' as if it were a third-party library.
Executing Targeted Commands
You can manage specific packages without navigating into their directories by using the --workspace (or -w) flag. This is executed from the root directory with standard user permissions.
To run a test script in a specific package:
npm run test --workspace=web-app
To add a dependency to a specific package:
npm install lodash --workspace=shared-utils
Risk: Running npm install inside a sub-package directory instead of the root can sometimes create a local node_modules folder, which may conflict with the hoisted root dependencies and lead to version mismatches.
Understanding Hoisting and its Limitations
Hoisting is the process where npm moves dependencies from individual package folders up to the root node_modules. This prevents the same version of a library (e.g., React) from being installed five times for five different packages.
The Phantom Dependency Problem
A significant side effect of hoisting is the phantom dependency. Because a package is hoisted to the root, any other package in the workspace can import it, even if that package didn't explicitly list it in its own package.json. This can lead to crashes in CI/CD pipelines where the environment is stricter than the local development setup.
Version Conflicts
If package-a requires lodash@4.0.0 and package-b requires lodash@3.0.0, npm cannot hoist both to the root. In this case, npm will hoist one version to the root and nest the conflicting version inside the specific package's node_modules folder. This increases disk usage and can cause "instanceof" check failures if the library maintains internal state.
Verification and Validation
To verify that your workspace is configured correctly, perform the following checks:
- Symlink Check: Run
ls -l node_modules(on Unix) ordir node_modules(on Windows). You should see a symbolic link for your local packages pointing back to thepackages/directory. - Lockfile Check: Ensure only one
package-lock.jsonexists at the root. If sub-packages have their own lockfiles, delete them and runnpm installfrom the root. - Dependency Resolution: Run a script in a consumer package that imports a local utility package to ensure the resolution path is active.
Rollback Procedure
To remove workspaces and return to independent packages:
- Remove the
workspacesfield from the rootpackage.json. - Delete the root
node_modulesandpackage-lock.json. - Navigate into each individual package directory and run
npm installto regenerate local dependencies.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.