npm 24 ๐ npm Workspaces (Monorepos)
An npm workspace is a repository whose root package.json contains a workspaces field listing the directories that hold other package.json files. Running npm install at the root installs every workspace’s dependencies in one pass and symlinks the local packages into the root node_modules, so they can depend on each other without being published to a registry . The feature is not a full monorepo tool; it solves linking and installation, not task orchestration, caching, or affected-package detection .
Key point: Workspaces solve the linking problem that npm link solves manually. They do not solve the orchestration problem that a build tool like Nx or Turborepo solves. A repository can use npm workspaces for installation and a separate tool for running tasks in dependency order.
Why npm workspaces exist
The manual linking problem. Before workspaces, a monorepo with interdependent packages required npm link in each package directory, followed by npm link <package> in every consumer. The links were manual, fragile, and not recorded in package.json. Workspaces automate the linking as part of npm install: any directory matched by the workspaces globs that contains a package.json is symlinked into the root node_modules .
The dependency duplication problem. Without workspaces, each package in a monorepo installs its own copy of every shared dependency. With workspaces, npm hoists dependencies to the root node_modules where versions allow, so a shared library like lodash exists once instead of once per package. When two packages require incompatible versions, the conflicting version is nested inside the individual package’s node_modules .
The publish problem. A workspace package can be published to the registry like any other package. The npm publish --workspaces flag publishes every workspace, and npm publish -w <name> publishes one . The symlinks are a local development convenience; the published artifact is a normal package that consumers install from the registry.
a. Defining Workspaces and Installing
The root package.json defines the workspace globs. The root package should be marked "private": true so it cannot be published accidentally .
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}
A typical layout separates applications from shared libraries:
my-monorepo/
โโโ package.json (private, workspaces field)
โโโ node_modules/ (hoisted dependencies, symlinked workspaces)
โโโ apps/
โ โโโ web/
โ โโโ package.json
โโโ packages/
โโโ shared-ui/
โ โโโ package.json
โโโ config/
โโโ package.json
Running npm install at the root installs all dependencies and creates symlinks in node_modules for each workspace package. A package in packages/shared-ui is symlinked as node_modules/shared-ui if its name is shared-ui .
To add a dependency to a specific workspace, use the -w flag:
# Add lodash to the web workspace only
npm install lodash -w web
# Add a workspace as a dependency of another workspace
# npm detects the local package and symlinks it instead of fetching
npm install shared-ui -w web
The resulting entry in apps/web/package.json uses a standard version range, but npm resolves it to the local symlink during development .
b. Running Scripts Across Workspaces
The --workspace flag (short form -w) targets one workspace, and --workspaces targets all of them. Without --if-present, npm errors when a workspace does not have the named script .
# Run build in one workspace
npm run build -w shared-ui
# Run build in all workspaces
npm run build --workspaces
# Run build in all workspaces that have a build script
npm run build --workspaces --if-present
The --workspaces flag runs the script in every workspace, in the order the workspaces are defined, not in dependency order. If shared-ui depends on config, and both have a build script, npm run build --workspaces does not guarantee that config builds before shared-ui . This is the limitation that motivates a build tool: npm has no dependency graph and no task ordering.
c. Hoisting, Phantom Dependencies, and Publishing
npm hoists dependencies to the root node_modules to avoid duplication. This means that a package can import a dependency that it did not declare in its own package.json, because the dependency is available at the root. This is a phantom dependency, and it breaks when the package is extracted from the workspace or the hoisting layout changes .
The rule is to declare every dependency that a workspace actually imports in that workspace’s package.json, even if npm would hoist it. The npm install <dep> -w <workspace> command adds the dependency to the correct workspace and makes the declaration explicit .
Publishing a workspace package uses the standard npm publish command, with the workspace flag to target the package :
# Publish one workspace
npm publish -w shared-ui
# Publish all workspaces
npm publish --workspaces
The published package contains the files allowed by its own files field and .npmignore, not the root’s. The symlink is irrelevant to the published artifact.
Complete Example Session
# ============================================
# PART 1: CREATE THE ROOT PACKAGE
# ============================================
mkdir my-monorepo && cd my-monorepo
npm init -y
# Edit package.json:
# { "private": true, "workspaces": ["packages/*"] }
# ============================================
# PART 2: CREATE A WORKSPACE
# ============================================
npm init -w ./packages/shared-ui
# Creates packages/shared-ui/package.json
# Adds "packages/*" to workspaces if not present
# ============================================
# PART 3: INSTALL AT THE ROOT
# ============================================
npm install
# Installs all workspace dependencies
# Symlinks packages/shared-ui to node_modules/shared-ui
# ============================================
# PART 4: ADD A DEPENDENCY TO ONE WORKSPACE
# ============================================
npm install lodash -w shared-ui
# lodash is added to packages/shared-ui/package.json
# ============================================
# PART 5: DEPEND ON A WORKSPACE FROM ANOTHER
# ============================================
npm init -w ./packages/web
npm install shared-ui -w web
# npm detects shared-ui is a workspace and symlinks it
# ============================================
# PART 6: RUN A SCRIPT IN ONE WORKSPACE
# ============================================
npm run build -w shared-ui
# ============================================
# PART 7: RUN A SCRIPT IN ALL WORKSPACES
# ============================================
npm run build --workspaces --if-present
# ============================================
# PART 8: PUBLISH A WORKSPACE
# ============================================
npm publish -w shared-ui
The eight parts trace the workspace lifecycle from root creation through local linking and publishing. Part 5 shows the automatic symlink that replaces npm link.
Quick Reference
Workspace Commands
| Command | Purpose |
|---|---|
npm init -w ./packages/a | Create a new workspace |
npm install | Install all, symlink workspaces |
npm install <dep> -w <name> | Add dependency to one workspace |
npm install <workspace> -w <name> | Add local workspace as dependency |
npm run <script> -w <name> | Run script in one workspace |
npm run <script> --workspaces | Run script in all workspaces |
--if-present | Skip workspaces without the script |
npm publish -w <name> | Publish one workspace |
npm publish --workspaces | Publish all workspaces |
Hoisting Behavior
| Situation | Result |
|---|---|
| Compatible versions | Hoisted to root node_modules |
| Incompatible versions | Conflicting version nested in package |
| Undeclared dependency | Available at root (phantom) |
Workspace vs. Build Tool
| Concern | npm Workspaces | Build Tool (Nx, Turborepo) |
|---|---|---|
| Linking packages | Yes | No |
| Installing dependencies | Yes | No |
| Task ordering by dependency | No | Yes |
| Caching | No | Yes |
| Affected-package detection | No | Yes |
Best Practices
โ Do This:
// Mark the root as private
{ "private": true, "workspaces": ["packages/*"] } // โ
# Declare every dependency in the workspace that imports it
npm install lodash -w shared-ui // โ
# Use --if-present when running scripts across all workspaces
npm run build --workspaces --if-present // โ
โ Don’t Do This:
// Publish the root package
{ "private": false } // โ
# Import a hoisted dependency without declaring it
// packages/web/index.js imports lodash without it in package.json // โ
# Assume --workspaces runs tasks in dependency order
npm run build --workspaces // order is definition order, not graph // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Phantom dependency | Import works due to hoisting | Declare in workspace package.json |
| Script runs in wrong order | --workspaces uses definition order | Use a build tool for dependency order |
| Workspace not linked | npm install not run at root | Run npm install at root |
| Cannot publish root | Root not private | Set "private": true |
| Version conflict | Two workspaces need different versions | Let npm nest the conflicting version |
Real-World Examples
1. Root Workspace Field
"workspaces": ["packages/*", "apps/*"]
2. Create Workspace
npm init -w ./packages/shared-ui
3. Install All
npm install
4. Add Dependency to Workspace
npm install lodash -w shared-ui
5. Local Workspace Dependency
npm install shared-ui -w web
6. Run One Script
npm run build -w shared-ui
7. Run All Scripts
npm run build --workspaces --if-present
8. Publish One
npm publish -w shared-ui
9. Publish All
npm publish --workspaces
10. Deduplicate
npm dedupe
Visual
Workspace Linking
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ my-monorepo/ โ
โ โโโ package.json (workspaces: ["packages/*"]) โ
โ โโโ node_modules/ โ
โ โ โโโ lodash/ (hoisted) โ
โ โ โโโ shared-ui -> ../packages/shared-ui โ
โ โโโ packages/ โ
โ โโโ shared-ui/ โ
โ โโโ package.json โ
โ โ
โ npm install creates the symlink automatically. โ
โ No npm link required. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hoisting and Nesting
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ROOT node_modules/ โ
โ โโโ lodash@4.17.21 (shared by both) โ
โ โโโ shared-ui -> packages/shared-ui โ
โ โ
โ packages/web/node_modules/ โ
โ โโโ lodash@3.10.1 (conflicting version nested) โ
โ โ
โ Compatible versions hoist. Incompatible versions nest. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Workspace vs. Build Tool
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ npm workspaces solve: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Linking packages โโ
โ โ Installing dependencies โโ
โ โ Symlinking local packages โโ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ A build tool solves: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Task ordering by dependency graph โโ
โ โ Caching โโ
โ โ Affected-package detection โโ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Workspaces are the foundation. โ
โ The build tool is the orchestration layer. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Root field | "workspaces": ["packages/*"] |
| Root privacy | Must be "private": true |
| Install | npm install at root |
| Link | Automatic symlink in root node_modules |
| Add dependency | npm install <dep> -w <name> |
| Run one script | npm run <script> -w <name> |
| Run all scripts | npm run <script> --workspaces |
| Publish one | npm publish -w <name> |
| Publish all | npm publish --workspaces |
| Hoisting | Root node_modules, conflicts nested |
Key takeaways:
- Workspaces automate the linking that
npm linkdid manually. Runningnpm installat the root symlinks every workspace package into the rootnode_modules, so a package can depend on another workspace package by name without a registry round-trip . - Dependencies hoist to the root, and conflicts nest. Compatible versions are installed once at the root. Incompatible versions are nested inside the package that needs them. The consequence is that a package can import a hoisted dependency without declaring it, which is a phantom dependency that breaks when the package is extracted .
--workspacesdoes not order tasks by dependency. It runs the script in every workspace in definition order. Ifshared-uidepends onconfig,npm run build --workspacesdoes not guaranteeconfigbuilds first. A build tool is required for dependency-ordered execution .- The root package should be private.
"private": trueprevents the root from being published accidentally and makes the distinction between the monorepo root and the publishable workspace packages explicit . - Publishing a workspace is a normal publish.
npm publish -w <name>publishes one workspace;npm publish --workspacespublishes all. The published artifact contains the workspace’s ownfilesand.npmignorerules, not the root’s .
Remember: npm workspaces are a linking and installation feature. They replace the manual npm link workflow with an automatic symlink that is created by npm install. They hoist shared dependencies and nest conflicting ones. They do not provide task orchestration, caching, or affected-package detection; those are the responsibility of a build tool that runs on top of the workspace. The private: true root and the explicit declaration of every dependency in each workspace’s package.json are the two practices that keep the workspace portable and prevent phantom dependencies from becoming production failures.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!