Skip to main content

Best npm Workspaces Alternatives 2026

·PkgPulse Team
0

TL;DR

npm workspaces are the least capable option — pnpm workspaces are the 2026 default for monorepos. npm workspaces lack per-workspace filtering, have no dependency isolation (all packages see all deps via hoisting), and have slower install. pnpm workspaces + Turborepo or Moon is the dominant stack: pnpm handles package resolution and isolation, Turborepo handles task caching. Yarn Berry (PnP mode) is a third option for teams already on Yarn.

Key Takeaways

  • pnpm workspaces: Best filtering (--filter), strictest isolation, pnpm-workspace.yaml
  • Bun workspaces: Fastest install, basic filtering, JSON-only config
  • npm workspaces: Universal but limited filtering, no isolation, slowest
  • Yarn Berry (PnP): Zero node_modules, but PnP compatibility issues persist in 2026
  • Combine with: Turborepo, Nx, or Moon for task caching and orchestration
  • Protocol: workspace:* in pnpm and Yarn links local packages cleanly

Why npm Workspaces Fall Short

npm introduced workspaces in npm 7, and the feature works — up to a point. For a monorepo with two or three packages, npm workspaces are perfectly adequate. For anything larger, the limitations become painful.

The core problem is hoisting. npm workspaces install all dependencies into the root node_modules directory. This means a package in packages/ui can accidentally import a dependency declared by packages/api without any error. These phantom dependencies work locally but can fail after reorganizing your repo or on a clean CI install where hoisting behaves differently. The fundamental issue is that npm workspaces do not enforce the boundary between what a package declares and what it can access.

The second limitation is filtering. pnpm's --filter can run a command in a specific package plus all its dependencies (pnpm --filter web...), or in all packages that depend on a given package (pnpm --filter ...@myapp/ui). npm offers no equivalent — you can run a command in a specific workspace by name, but graph-based traversal is absent.

The third limitation is speed. npm installs are roughly three times slower than pnpm installs on a comparable monorepo, because pnpm uses a content-addressable store where packages are downloaded once and hard-linked rather than copied.


Package Health Table

ToolWeekly DownloadsIsolationGraph FilterInstall Speedworkspace: protocol
npm workspaces~25M (npm)No (hoisted)NoSlowNo
pnpm workspaces~10MYes (strict)YesFastYes
Bun workspaces~3MPartialBasicFastestNo (experimental)
Yarn Berry~3MYes (PnP)YesFastYes

pnpm Workspaces: The Standard

pnpm is the dominant monorepo package manager in 2026. Its content-addressable store means each version of every package is stored once on disk and hard-linked into each project that uses it — reducing both install time and disk usage compared to npm. Strict isolation means each package can only access the dependencies it declares, eliminating phantom dependency bugs.

The pnpm-workspace.yaml file at the repo root declares which directories contain packages:

# pnpm-workspace.yaml (root):
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'
  - '!**/node_modules/**'

Local packages reference each other using the workspace:* protocol. When you publish, pnpm replaces workspace:* with the actual version number automatically.

// packages/ui/package.json — local dependency:
{
  "name": "@myapp/ui",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}
// apps/web/package.json — consuming the local package:
{
  "name": "web",
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "react": "^19.0.0"
  }
}

pnpm Filtering

The --filter flag is pnpm's most powerful monorepo feature. It lets you scope commands to specific packages, their dependencies, or packages that depend on them:

# Run command in specific package:
pnpm --filter web run dev
pnpm --filter @myapp/ui run build

# Run in package and all its dependencies (build deps first):
pnpm --filter web... run build

# Run in packages that depend on @myapp/ui:
pnpm --filter ...@myapp/ui run build

# Run in all packages matching glob:
pnpm --filter './packages/**' run test

# Add dep to specific workspace:
pnpm --filter web add axios
pnpm --filter @myapp/ui add -D vitest

# Run in all packages:
pnpm -r run build       # Recursive
pnpm -r run test --if-present  # Only if test script exists

pnpm Workspace Isolation

The isolation model is a key differentiator:

pnpm workspace isolation (default):
  packages can only import what they explicitly declare
  symlinks to packages/@myapp/ui (not copied)
  phantom dependencies cause errors (not silently allowed)
  each package node_modules contains only what it declares

npm workspace (hoisted, no isolation):
  all packages share root node_modules
  apps/web can accidentally import packages/ui's deps
  phantom dependencies silently work until they don't
  this is why "works on my machine" happens in npm monorepos

npm Workspaces: Universal Baseline

npm workspaces require no additional tooling — they are built into npm since version 7. If your monorepo has two or three packages and you are already on npm, workspaces work without adding a new package manager.

// package.json (root):
{
  "name": "my-monorepo",
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}
# npm workspace commands:
npm install                         # Install all workspaces
npm install --workspace=apps/web    # Install specific workspace
npm run build --workspace=apps/web  # Run in specific workspace
npm run build --workspaces          # Run in all workspaces
npm run build --workspaces --if-present  # Skip if no script

The gaps relative to pnpm become apparent as the monorepo grows:

  • No --filter with dependency graph traversal
  • No "build web and all its deps" command
  • No strict isolation (phantom dependencies silently allowed)
  • Install is roughly three times slower than pnpm
  • No workspace:* protocol (uses relative paths instead)
  • No way to enforce that apps/web cannot access packages/api's dependencies

npm workspaces are a reasonable starting point for small repos. The upgrade path to pnpm is straightforward — delete node_modules, create pnpm-workspace.yaml, and change the package references from relative paths to workspace:*. Most teams make this move when the npm limitations start causing real problems at around five or more packages.


Bun Workspaces: Speed-First

Bun workspaces use the same workspaces field in package.json as npm — there is no separate config file:

// package.json (root) — same format as npm:
{
  "name": "my-monorepo",
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}
# Bun workspace commands:
bun install                          # Install all (fast!)
bun add react --workspace apps/web   # Add to specific workspace
bun run --filter '*' build           # Run in all workspaces
bun run --filter 'apps/*' dev        # Run in apps only

Bun's install is the fastest of any package manager — typically 5–10x faster than npm and 2–3x faster than pnpm on cold installs. For monorepos with many packages and frequent clean installs (common in CI), this speed difference is meaningful.

The trade-offs: Bun's --filter is glob-based only, with no dependency graph traversal. The workspace:* protocol is experimental and has compatibility issues with some publishing workflows. Some packages with native addons or unusual install scripts do not install correctly with Bun. In 2026, Bun workspaces are best suited for smaller monorepos where install speed is the top priority and the compatibility edge cases have been validated against your specific dependencies.


Yarn Berry (PnP): Zero node_modules

Yarn Berry (Yarn 2+) introduced Plug'n'Play (PnP) as an alternative to the node_modules directory. In PnP mode, packages are stored in a zip cache and resolution happens via a generated .pnp.cjs file rather than directory traversal. There is no node_modules folder at all.

# Enable Yarn Berry in existing project:
yarn set version stable
# .yarnrc.yml:
nodeLinker: pnp   # Plug'n'Play (no node_modules)
# Or:
nodeLinker: node-modules  # Traditional (more compatible)
# Yarn workspaces commands:
yarn workspace web add axios         # Add to specific workspace
yarn workspaces foreach run build    # Run in all workspaces
yarn workspaces foreach -p run build # Parallel across workspaces
yarn workspaces foreach -A --topological run build  # Topological order

Yarn Berry workspaces support graph-aware filtering similar to pnpm, and the workspace:* protocol works the same way. The strict isolation in PnP mode is even stricter than pnpm — packages cannot access anything not declared in their own package.json.

The challenge in 2026 is compatibility. PnP requires every tool that resolves modules to understand the PnP resolution algorithm. Most major tools now have PnP support (Webpack, Rollup, Vitest, Next.js), but you may encounter edge cases with less common packages or CLI tools. The Yarn SDK configuration for editors (VS Code, JetBrains) adds setup overhead.

Yarn Berry's node-modules linker mode (set in .yarnrc.yml) is fully compatible with all tools but loses the PnP advantages. Teams that want Yarn's foreach syntax and workspace:* without PnP compatibility risk can use this mode.


Turborepo + pnpm: The Dominant Stack

The package manager handles installation and dependency isolation. The task runner handles build orchestration and caching. In 2026, the dominant combination is pnpm for packages and Turborepo for tasks.

Turborepo's key feature is caching: it hashes the inputs to each task (source files, env vars, dependencies) and skips the task if the output cache is still valid. The first turbo run build builds everything. The second run with no changes completes in milliseconds because all tasks hit the cache.

# Install Turborepo:
npm install --save-dev turbo
# Or globally:
npm install -g turbo
// turbo.json — task dependency graph:
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

The ^build syntax means "run build in all dependencies of this package first". This handles the common monorepo problem of running tasks in the correct topological order automatically.

# Turborepo commands:
turbo run build                    # Build all packages (cached)
turbo run build --filter=web       # Build only web (and its deps)
turbo run build --filter=...@myapp/ui  # Build all that depend on @myapp/ui
turbo run dev                      # Run dev servers (not cached)
turbo run build --dry              # Show what would run without running

Vercel provides a remote cache for Turborepo that can be shared across team members and CI runs. With remote cache, a CI run with no changes to packages/ui will skip the packages/ui build and download the cached output instead — even on a fresh CI instance.

The recommended full stack for most JavaScript monorepos in 2026:

Package manager:  pnpm workspaces
Task runner:      Turborepo v2 (Rust-based scheduler)
CI caching:       Vercel Remote Cache (Turborepo) or GitHub Actions cache

Setup steps:
  1. pnpm-workspace.yaml with apps/* and packages/* globs
  2. turbo.json with build/test/lint/dev tasks
  3. workspace:* protocol in all internal package deps
  4. pnpm --filter for workspace-specific commands

Comparison Table

FeaturepnpmBunnpmYarn Berry
Install speedFastFastestSlowFast
Disk usageLowLowHighLowest (PnP)
IsolationStrictPartialNoneStrict
Graph filteringYes (--filter)PartialNoYes
workspace: protocolYesNo (experimental)NoYes
CompatibilityHighMediumHighestMedium
Turborepo supportYesYesYesYes
Nx supportYesYesYesYes

When to Choose

pnpm workspaces is the right default for most JavaScript monorepos in 2026. Strict isolation eliminates phantom dependency bugs. The --filter graph traversal simplifies running commands in dependency order. Content-addressable storage means fast installs and low disk usage. If you are starting a new monorepo, start with pnpm.

npm workspaces make sense for small repos (two to three packages) where the limitations have not yet become painful, or for teams that need maximum tooling compatibility without installing a new package manager. The upgrade path to pnpm is easy when you outgrow it.

Bun workspaces are worth considering for small monorepos where install speed is the priority and you have validated that your specific dependencies are all Bun-compatible. The compatibility surface area is still smaller than pnpm.

Yarn Berry is the right choice for teams already invested in the Yarn ecosystem who want the workspace:* protocol and foreach syntax. PnP mode is technically impressive but requires editor and tool configuration that adds setup overhead.


Choosing a Monorepo Tool by Team Size

The right monorepo toolchain is not the same for a two-person startup as it is for a fifty-person engineering organization. At the small end, the overhead of configuring Turborepo or Nx is often not worth the benefit — a two-package monorepo with pnpm -r run build and no caching is simple to understand and debug. Add Turborepo when you have five or more packages and developers start complaining that CI rebuilds everything on every change. The caching benefit becomes meaningful at that scale, and the turbo.json configuration is small enough to be maintained by any team member.

For medium-sized teams (ten to thirty engineers) with active monorepos across multiple domains, Turborepo's remote cache is the key value. Sharing build and test caches across developers and CI means that after the first run of the day, most developers never wait for a full build — they're always pulling from cache. This changes the developer experience meaningfully. The configuration investment is modest: a turbo.json with task dependency declarations and a Vercel remote cache token in CI environment variables.

Nx is worth evaluating when the monorepo grows to the enterprise scale — dozens of packages, multiple teams, strict ownership boundaries, and complex dependency graphs. Nx's project graph visualization, code owners enforcement, and affected-package detection (only run tasks in packages that changed since the last main branch merge) are genuinely valuable at this scale. The trade-off is configuration complexity: Nx's project.json files, generators, and executor system have a steeper learning curve than Turborepo's single turbo.json. Teams that hit the Turborepo ceiling typically find Nx's additional surface area justified by the debugging and ownership capabilities it provides.


Migrating from a Single Repository

The most common migration path is extracting packages from a growing single-repo application into a monorepo structure. The recommended approach is incremental rather than a big-bang rewrite. First, introduce the workspace tooling alongside the existing application: add pnpm-workspace.yaml, move the application to apps/my-app, and verify that the existing application still builds and tests correctly. This validates the workspace setup without breaking anything.

The second step is identifying the first extraction candidate — typically a utility module that multiple applications or future applications would benefit from sharing. Move it to packages/utils, update its imports to use the workspace:* protocol, and confirm that the consuming application still works. This first extraction is always the most instructive: it surfaces any implicit assumptions your code makes about module resolution, any path aliasing in your build config that needs updating, and any CI steps that assumed a single-package structure.

Resist the urge to extract too aggressively too early. A monorepo with twenty packages where ten of them are trivial single-file utilities creates maintenance overhead without meaningfully improving code sharing. The useful rule of thumb is: extract a package when two different teams or two different deployment contexts need the same code. Before that threshold, keeping code collocated in the application is simpler than managing cross-package versioning and dependency declarations.


The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.