Skip to content
Back to Knowledge Base

npm Workspaces: Setup, Commands, and Best Practices

npm workspaces manage multiple packages in a single repository (or a monorepo) from one root package.json. You list package folders in the workspaces field, and npm install installs all of their dependencies in one pass, symlinking local packages into the root node_modules so they can depend on each other without publishing to a registry.

For the full configuration reference, see the npm workspaces documentation.

An npm workspace is a repository whose root package.json has a workspaces field listing the directories that hold package.json files. The root must be private so it can't be published by accident:

package.json
{
"name": "my-workspace",
"private": true,
"workspaces": ["apps/*", "packages/*"],
}

Every directory matched by those globs that contains a package.json becomes a workspace. A common layout separates applications from shared packages:

  • Directorymy-workspace/
    • Directoryapps/
      • Directoryweb/
        • package.json
    • Directorypackages/
      • Directoryshared-ui/
        • package.json
    • package.json

npm doesn't use a workspace: protocol. To depend on another package in the workspace, reference it by name with * or a semver range that matches the local version, and npm symlinks the local package instead of fetching it from the registry:

apps/web/package.json
{
"name": "web",
"dependencies": {
"react": "^19.0.0",
"@my-workspace/shared-ui": "*",
},
}

Run npm install once at the root to install every package's dependencies and link the internal ones.

Run scripts with --workspace and --workspaces

Section titled “Run scripts with --workspace and --workspaces”

The --workspace flag (short form -w) targets one workspace, and --workspaces targets all of them. Without --if-present, npm errors on any workspace that's missing the script:

Terminal window
npm run build --workspaces --if-present # every package with a build script
npm run build --workspace=web # a single package
npm install axios -w web # add a dependency to one package

Pass --workspace multiple times to target several packages in one command. npm install b -w a where b is another workspace adds it as a symlinked local dependency.

Can npm workspaces filter by changed packages?

Section titled “Can npm workspaces filter by changed packages?”

No. npm targets workspaces by name, but it has no equivalent of the pnpm --filter flag for selecting packages by dependency relationship or by what changed in git. Your options are to run every script on every push, maintain a wrapper script around git diff, or adopt a build tool that computes affected projects from a dependency graph.

Running everything is fine while the repository is small. The cost grows linearly with package count, and it's usually CI time that forces the move to one of the other two options.

npm installs workspace dependencies into the root node_modules wherever versions allow, so shared dependencies exist once. When two packages need conflicting versions, the extra version is nested inside the individual package's node_modules. Run npm dedupe to flatten duplicates that accumulate over time.

Hoisting has a downside: any package can import any hoisted dependency, including ones it never declared. These phantom dependencies break when a package is extracted from the workspace or the hoisting layout shifts. Declare everything a package imports in its own package.json.

Do npm workspaces replace a monorepo tool?

Section titled “Do npm workspaces replace a monorepo tool?”

No. npm workspaces install and link packages, but they don't order tasks by dependency, cache results, or detect which projects a commit affects, so CI reruns every task on every push. Workspaces solve linking, and a build tool solves orchestration once task times grow.

When that becomes the bottleneck, add Nx on top without changing how npm works:

Terminal window
npx nx@latest init

Your existing package.json scripts keep working and npm keeps managing installs.

Before Nx, npm runs a script across packages, but it runs every task every time and has no notion of which packages a change affects:

Terminal window
npm run build --workspaces --if-present # every package
npm run build --workspace=web # a single package

After nx init, run the same scripts through Nx:

Terminal window
nx run-many -t build # every project
nx build web # a single project
nx affected -t build # only projects touched by your changes

The first run executes your scripts. A second run with no changes is restored from the cache, and nx affected skips the projects your change does not touch.

nx init writes an nx.json with targetDefaults that control how targets behave:

nx.json
{
"targetDefaults": {
"build": {
"cache": true,
"dependsOn": ["^build"],
},
"dev": {
"continuous": true,
},
},
}

cache: true makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. Nothing is cached unless you opt in, so you stay in control. dependsOn: ["^build"] builds a project's dependencies first. continuous: true marks tasks that don't exit, like dev servers and watchers, so tasks that depend on them don't wait for them to complete.

Nx Cloud adds more ways to speed up CI for a npm workspace: remote caching shares the cache across CI runs and teammates, Nx Agents distribute tasks across machines, and self-healing CI proposes fixes when tasks fail. For the full walkthrough, see Adding Nx to an existing monorepo and CI setup.

Working with a different package manager? See pnpm workspaces, Yarn workspaces, or Bun workspaces. New to monorepos? Start with what a monorepo is and why teams use one.

Last updated: