Mastering npm Workspaces: Unify Dependencies in a Monorepo
Learn how npm workspaces unify dependency resolution, hoist shared packages, and run cross‑package scripts across a monorepo. Follow a step‑by‑step example, see trade‑offs, and finish with actionable tips.
08 Sept 2025, 10:03 UTC

Problem: Fragmented Dependency Management in Monorepos
When a project grows into multiple packages—say a UI library, a server API, and a CLI tool—each package traditionally maintains its own package.json and node_modules. This fragmentation leads to duplicated dependencies, inconsistent lockfiles, and a headache when upgrading shared libraries.
Thesis: npm Workspaces Bring a Single Source of Truth
Starting with npm 7, the workspaces field lets you declare all packages in a single top‑level package.json. npm then creates one shared package-lock.json, hoists common dependencies to the repo root, and runs scripts in the correct context. The result is a cleaner, faster, and more reliable monorepo.
Core Concepts
- Unified lockfile: One
package-lock.jsonfor the entire repo. - Hoisting: Shared dependencies are installed in the root
node_modules, reducing duplication. - Workspace scripts: Scripts can be defined globally or per‑package; npm resolves them automatically.
- Stable protocol: From npm 7 onward, the workspace API is stable, but hoisting rules and flags evolve.
Concrete Setup Example
# Project layout
root/ # root package.json & lockfile
├─ packages/
│ ├─ core/
│ │ └─ package.json
│ └─ ui/
│ └─ package.json
Root package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "npm run build --workspaces"
}
}
Core package packages/core/package.json:
{
"name": "@myorg/core",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"lodash": "^4.17.21"
}
}
UI package packages/ui/package.json depends on core:
{
"name": "@myorg/ui",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@myorg/core": "1.0.0"
}
}
Run npm install from the root. npm will:
- Create a single
package-lock.json. - Hoist
lodashtoroot/node_modulesbecause both packages use it. - Link
@myorg/coreintoui/node_modulesas a workspace dependency.
Running Workspace Scripts
Define a build script in each package:
// packages/core/package.json
"scripts": { "build": "echo Building core" }
// packages/ui/package.json
"scripts": { "build": "echo Building UI" }
From the root, execute npm run build --workspaces. npm will run each build script in its own context and output:
packages/core: Running build
Building core
packages/ui: Running build
Building UI
Running npm run build inside packages/ui only triggers that package’s script, demonstrating correct isolation.
Diagnostic: Checking Hoisting and Lockfile
After installation, inspect the root node_modules:
root/node_modules
├─ lodash
└─ @myorg
└─ core
Verify that lodash exists only once. If you see duplicates, run npm dedupe to clean up.
Trade‑offs and Limitations
- Disk Footprint: Hoisted root
node_modulesmay contain packages not used by any workspace, slightly increasing size. - Circular Dependencies: npm will refuse to install if workspaces reference each other in a cycle. Validate the dependency graph with
npm lsbefore committing. - Publishing: The root package is often marked
privateto avoid accidental publish. Usenpm publish --workspace @myorg/coreto target a specific package. - Older npm Versions: npm 6 and below ignore the
workspacesfield. Ensure the CI environment uses npm >= 7.
Actionable Takeaway
Adopt npm workspaces if you:
- Maintain multiple interrelated packages.
- Want a single, consistent lockfile.
- Need to run cross‑package scripts without manual orchestration.
Start by adding a workspaces array to your root package.json, move existing packages under packages/*, run npm install, and verify hoisting. From there, you can add shared scripts, enforce dependency rules, and streamline CI pipelines.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.