| |

npm 24 ๐ŸŒ npm Workspaces (Monorepos)

โฑ๏ธ Estimated read: 9 min | |

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

CommandPurpose
npm init -w ./packages/aCreate a new workspace
npm installInstall 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> --workspacesRun script in all workspaces
--if-presentSkip workspaces without the script
npm publish -w <name>Publish one workspace
npm publish --workspacesPublish all workspaces

Hoisting Behavior

SituationResult
Compatible versionsHoisted to root node_modules
Incompatible versionsConflicting version nested in package
Undeclared dependencyAvailable at root (phantom)

Workspace vs. Build Tool

Concernnpm WorkspacesBuild Tool (Nx, Turborepo)
Linking packagesYesNo
Installing dependenciesYesNo
Task ordering by dependencyNoYes
CachingNoYes
Affected-package detectionNoYes

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

PitfallWhy It HappensFix
Phantom dependencyImport works due to hoistingDeclare in workspace package.json
Script runs in wrong order--workspaces uses definition orderUse a build tool for dependency order
Workspace not linkednpm install not run at rootRun npm install at root
Cannot publish rootRoot not privateSet "private": true
Version conflictTwo workspaces need different versionsLet 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

ItemValue
Root field"workspaces": ["packages/*"]
Root privacyMust be "private": true
Installnpm install at root
LinkAutomatic symlink in root node_modules
Add dependencynpm install <dep> -w <name>
Run one scriptnpm run <script> -w <name>
Run all scriptsnpm run <script> --workspaces
Publish onenpm publish -w <name>
Publish allnpm publish --workspaces
HoistingRoot node_modules, conflicts nested

Key takeaways:

  • Workspaces automate the linking that npm link did manually. Running npm install at the root symlinks every workspace package into the root node_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 .
  • --workspaces does not order tasks by dependency. It runs the script in every workspace in definition order. If shared-ui depends on config, npm run build --workspaces does not guarantee config builds first. A build tool is required for dependency-ordered execution .
  • The root package should be private. "private": true prevents 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 --workspaces publishes all. The published artifact contains the workspace’s own files and .npmignore rules, 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.

Was this helpful?


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!