npm Workspaces: A Practical Monorepo Decision with Real Trade-offs
npm workspaces solves monorepo dependency duplication with hoisting and a single lockfile, but phantom dependencies and peer behavior changes require deliberate handling. A practical walkthrough with verification steps.
08 Aug 2025, 03:07 UTC

The Problem: Dependency Duplication and Version Drift
You're managing a monorepo with three packages: a shared UI library, a backend service, and a CLI tool. Each has its own package.json. Running npm install in each directory creates three separate node_modules trees, duplicating common dependencies like TypeScript, ESLint, and your internal utilities. When you update a shared dependency in one package, the others drift out of sync. CI builds become non-deterministic because lockfiles diverge.
npm workspaces, stable since npm v7, addresses this by letting a single root package.json declare workspace paths. npm install then hoists shared dependencies to the root node_modules and symlinks local packages via file: references. The result: one lockfile, one install, and local changes visible immediately across packages without publishing.
How Workspaces Change the Install Graph
Add a workspaces field to your root package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}
Run npm install at the repository root. npm creates a single package-lock.json at the root, hoists compatible dependencies to node_modules/, and places symlinks for each workspace package under node_modules/. Each workspace's node_modules becomes a sparse tree pointing upward.
Verification: After install, check ls -la node_modules/ for symlinks like ui-lib -> ../../packages/ui-lib and confirm shared deps such as typescript exist only at the root.
Script Orchestration Across Packages
Use npm run --workspaces to execute a script in every workspace that defines it, or npm run --workspace <name> for a single package. The --if-present flag skips packages missing the script rather than failing.
# Run tests in all packages that have a "test" script
npm run --workspaces --if-present test
# Build only the CLI package
npm run --workspace cli build
Run these from the repository root. This keeps CI configuration minimal: a single npm ci followed by npm run --workspaces --if-present test covers the whole monorepo.
Lockfile Reproducibility and the overrides Escape Hatch
npm ci consumes the root package-lock.json for deterministic installs in CI. When a transitive dependency needs a forced version across all workspaces, add an overrides block to the root package.json:
{
"overrides": {
"lodash": "^4.17.21"
}
}
This rewrites the resolution for lodash everywhere in the workspace graph. Use sparingly: forcing versions can mask incompatibilities and break packages that rely on specific transitive APIs. Always run the full test suite after adding overrides.
Trade-offs You'll Encounter
- Phantom dependencies: A package may import a hoisted dependency it never declared. This compiles locally but fails when the package is published standalone. Fix by verifying declared dependencies match actual imports.
- Peer dependency behavior changed between npm v7 and v8: v7 auto-installed peers; v8 treats them as optional. If your workspaces rely on peer auto-installation, pin npm in CI or declare peers explicitly in each workspace.
- Overrides are blunt: They apply globally. A version that fixes a vulnerability in one package may break another. Prefer updating the direct dependency in the affected workspace first.
Actionable Next Steps
- Confirm npm version:
npm --versionneed >=7.0.0 for stable workspaces. - Create a minimal root
package.jsonwithworkspaces: ["packages/*"]and runnpm install. - Inspect
node_modules/for hoisted packages and workspace symlinks. - Add a shared script to each workspace
package.json, then runnpm run --workspaces --if-present test. - Commit the root
package-lock.jsonand verify CI withnpm ci && npm run --workspaces --if-present test.
If phantom dependencies surface, add missing entries to each workspace's dependencies or devDependencies. If overrides become necessary, document the reason and schedule a review to remove them once upstream packages update.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.