|$ curl https://forge-ai.dev/api/markdown?path=docs/npm/workspaces
$cat docs/npm-—-workspaces-&-monorepos.md
updated Recently·14 min read·published

npm — Workspaces & Monorepos

npmIntermediate
Introduction

A monorepo (monolithic repository) stores multiple related projects in a single repository. npm workspaces provide built-in support for managing monorepos by allowing you to install dependencies for multiple packages with a single npm install command.

This guide covers setting up npm workspaces, the workspace protocol, dependency hoisting, comparing workspace implementations, and integrating with monorepo tools like Turborepo and Nx.

What are npm Workspaces?

npm workspaces (introduced in npm v7) allow you to define a set of packages within a single root package.json. When you run npm install at the root, npm installs dependencies for all workspace packages simultaneously and symlinks interdependent packages.

Key benefits of workspaces:

Single install

One npm install installs all dependencies for all packages

Symlinked packages

Interdependent packages are symlinked, so changes are immediately reflected

Hoisted dependencies

Shared dependencies are hoisted to the root node_modules, reducing duplication

Unified lock file

Single package-lock.json for the entire monorepo

Setting Up Workspaces

To set up npm workspaces, add the workspaces field to your root package.json. This field accepts an array of glob patterns pointing to your packages.

package.json
JSON
1// Root package.json
2{
3 "name": "my-monorepo",
4 "private": true,
5 "workspaces": [
6 "packages/*",
7 "apps/*",
8 "shared/*"
9 ]
10}
11
12// Directory structure:
13// my-monorepo/
14// package.json (root, with workspaces config)
15// packages/
16// ui/ (package name: @my/ui)
17// package.json
18// utils/ (package name: @my/utils)
19// package.json
20// apps/
21// web/ (package name: @my/web)
22// package.json
23// api/ (package name: @my/api)
24// package.json
25// shared/
26// config/ (package name: @my/config)
27// package.json
28// node_modules/
29// package-lock.json

Each package has its own package.json with a unique name:

package.json
JSON
1// packages/ui/package.json
2{
3 "name": "@my/ui",
4 "version": "1.0.0",
5 "dependencies": {
6 "react": "^18.3.0",
7 "@my/utils": "*"
8 }
9}
10
11// apps/web/package.json
12{
13 "name": "@my/web",
14 "version": "1.0.0",
15 "dependencies": {
16 "@my/ui": "*",
17 "@my/utils": "*",
18 "next": "^14.0.0"
19 }
20}
21
22// Install everything from root:
23cd my-monorepo && npm install
24// Installs deps for all 4 packages, symlinks @my/ui, @my/utils
The workspace: Protocol

npm workspaces support the workspace: protocol for specifying inter-package dependencies. This protocol ensures the dependency is always resolved from the local workspace, not the registry.

package.json
JSON
1// Using workspace: protocol (npm v8+)
2{
3 "dependencies": {
4 "@my/ui": "workspace:*",
5 "@my/utils": "workspace:^",
6 "@my/config": "workspace:1.0.0"
7 }
8}
9
10// workspace:* — Always use the local workspace version
11// workspace:^ — Use local workspace, allow minor/patch
12// workspace:~ — Use local workspace, allow patch only
13// workspace:1.0.0 — Use local workspace at exact version
14
15// Before publishing, workspace: dependencies are
16// automatically replaced with the actual version:
17// "workspace:*" -> "1.0.0"
18// "workspace:^" -> "^1.0.0"
19
20// This is done by npm publish or npm pack.
21// If the workspace version is 1.0.0:
22// workspace:* -> 1.0.0
23// workspace:^ -> ^1.0.0
24// workspace:~ -> ~1.0.0
25// workspace:1.0.0 -> 1.0.0

info

Use workspace:* for all inter-package dependencies in a monorepo. It is the most flexible option and lets npm handle the version substitution automatically during publishing. This avoids version drift between packages.
Dependency Hoisting & Layout

npm workspaces use a hoisting algorithm to minimize disk usage. Shared dependencies are hoisted to the root node_modules, while conflicting versions are kept in individual package directories.

directory-layout
TEXT
1// node_modules layout with hoisting:
2my-monorepo/
3 node_modules/
4 react/ ← hoisted (shared by multiple packages)
5 lodash/ ← hoisted
6 next/ ← hoisted
7 .package-lock.json
8 packages/
9 ui/
10 node_modules/ ← only if react version conflicts
11 react/ ← different version than root
12 package.json
13 utils/
14 package.json ← no node_modules, uses hoisted deps
15 config/
16 package.json
17
18// How npm decides:
19// 1. Same version across packages → hoist to root
20// 2. Different versions → keep in package's node_modules
21// 3. Nested workspace packages → not hoisted (always symlinked)
22
23// pnpm uses a different approach: content-addressable store
24// npm and yarn use hoisting (same algorithm)

warning

Hoisting can cause phantom dependencies — a package can import a dependency it does not declare in its own package.json because it is hoisted to the root. Use ESLint rules like import/no-extraneous-dependencies to enforce that packages only import their declared dependencies.
Running Scripts Across Workspaces

npm workspaces provide several ways to run scripts across some or all packages:

terminal
Bash
1# Run a script in every workspace
2npm run build --workspaces
3
4# Run in specific workspaces
5npm run test --workspace=@my/ui --workspace=@my/utils
6
7# Run in all workspaces except some
8npm run lint --workspaces --if-present
9# --if-present: skip packages that don't have the script
10
11# Run a command from a specific workspace context
12npm run dev -w @my/web
13
14# Run install only for specific workspaces
15npm install -w @my/ui -w @my/utils
16
17# Execute command in workspace (npm 9+)
18npm exec --workspace=@my/ui -- vitest run
19
20# Run in order (dependencies first)
21npm run build --workspaces --include-workspace-root
22
23# List workspaces
24npm ls --workspaces
25npm ls -w @my/ui # tree for specific workspace
26
27# Check outdated deps across workspaces
28npm outdated --workspaces
Shared DevDependencies

Common dev tools (TypeScript, ESLint, Prettier, Vitest) should be installed at the root level and shared across all workspace packages. This ensures consistent tooling versions and reduces disk usage.

package.json
JSON
1// Root package.json — shared dev tools
2{
3 "name": "my-monorepo",
4 "private": true,
5 "workspaces": ["packages/*", "apps/*"],
6 "devDependencies": {
7 "typescript": "^5.4.0",
8 "eslint": "^8.57.0",
9 "prettier": "^3.2.0",
10 "vitest": "^1.6.0",
11 "@typescript-eslint/parser": "^7.0.0"
12 },
13 "scripts": {
14 "lint": "eslint packages/ apps/",
15 "format": "prettier --write packages/ apps/",
16 "typecheck": "tsc --noEmit -p tsconfig.json"
17 }
18}
19
20// Each package can add package-specific devDeps:
21// packages/ui/package.json
22{
23 "name": "@my/ui",
24 "devDependencies": {
25 "@testing-library/react": "^14.0.0",
26 "typescript": "*" // uses root version via hoisting
27 }
28}

best practice

Keep shared dev tools at the root and use per-package devDependencies only for package-specific tooling (like testing libraries). Use a root tsconfig.json with project references for TypeScript in monorepos.
Comparing Workspace Implementations

npm, Yarn, and pnpm all support workspaces, but they differ in their approach to dependency management:

FeaturenpmYarnpnpm
Introducedv7 (2020)v2 (2020)v5 (2021)
HoistingYes (flat)Yes (flat)No (content-addressable store)
Strict isolationNoNoYes (strict)
Disk usageModerate (hoisted)Moderate (hoisted)Low (single store, hard links)
Install speedFastFast (with PnP: fastest)Fast (cached store)
workspace: protocolYes (v8+)Yes (v2+)Yes (v5+)
Lock filepackage-lock.jsonyarn.lockpnpm-lock.yaml

info

pnpm's strict isolation prevents phantom dependencies entirely — packages can only import dependencies they explicitly declare. This catches a common class of bugs. If you are starting a new monorepo, pnpm is worth considering for its strictness and disk efficiency.
Monorepo Tooling (Turborepo, Nx)

npm workspaces handle dependency management, but monorepo tools like Turborepo and Nx add task orchestration — caching, parallel execution, and dependency-aware build ordering.

turbo.json
JSON
1// Turborepo — turbo.json
2{
3 "$schema": "https://turbo.build/schema.json",
4 "tasks": {
5 "build": {
6 "dependsOn": ["^build"],
7 "inputs": ["src/**", "tsconfig.json"],
8 "outputs": ["dist/**"],
9 "cache": true
10 },
11 "test": {
12 "dependsOn": ["build"],
13 "inputs": ["src/**", "tests/**"],
14 "cache": true
15 },
16 "lint": {
17 "dependsOn": [],
18 "cache": true
19 }
20 }
21}
22
23// Run tasks with Turborepo:
24npx turbo build # build all, respecting dependency order
25npx turbo test --filter=@my/ui # test only @my/ui
26npx turbo lint test # run lint then test in parallel
nx.json
JSON
1// Nx — nx.json
2{
3 "tasksRunnerOptions": {
4 "default": {
5 "runner": "nx/tasks-runners/default",
6 "options": {
7 "cacheableOperations": ["build", "test", "lint"]
8 }
9 }
10 },
11 "targetDefaults": {
12 "build": {
13 "dependsOn": ["^build"],
14 "outputs": ["{projectRoot}/dist"]
15 }
16 }
17}
18
19// Use with npm workspaces:
20npx nx run-many --target=build --parallel=3
21npx nx affected:test # only test changed projects
22npx nx graph # visualize project dependencies
🔥

pro tip

Turborepo is simpler to set up and great for most monorepos. Nx is more powerful for complex projects with many targets and advanced code generation. Both integrate seamlessly with npm workspaces — use whatever suits your team's needs.
Best Practices

Use workspace:* for Inter-Package Dependencies

Always use workspace:* for dependencies between packages in your monorepo. npm automatically resolves these to the correct local versions.

Keep the Root package.json Lean

The root package.json should be private: true and contain only shared devDependencies and workspace configuration. Never publish the root package.

Enforce Dependency Declarations

Use ESLint rules like import/no-extraneous-dependencies to ensure each package declares all its dependencies. This prevents phantom dependency bugs that only appear when hoisting changes.

Use a Task Runner for Large Monorepos

For monorepos with more than 10 packages, add Turborepo or Nx. Their caching and parallel execution can reduce CI times from hours to minutes.

$Blueprint — Engineering Documentation·Section ID: NPM-WS·Revision: 1.0