Guide to npm Workspaces for JavaScript Monorepos
npm workspaces provide a native solution within the Node Package Manager to manage multiple packages inside a single top-level repository, commonly known as a monorepo. This article explains what npm workspaces are, their core mechanisms for handling shared dependencies and internal package linking, and how to configure and execute commands across projects efficiently without relying on third-party tools.
What Are npm Workspaces?
Introduced in npm v7, workspaces are a set of features that allow developers to manage multiple packages within a single root package. Instead of maintaining isolated repositories for individual modules, components, or services, teams can house all related projects inside a single repository while keeping their individual package definitions intact.
This setup eliminates the overhead of publishing separate packages to a registry just to share code between internal projects during development.
How npm Workspaces Manage Monorepos
npm workspaces streamline monorepo workflows through three primary mechanisms: dependency hoisting, automatic symlinking, and coordinated command execution.
1. Dependency Hoisting
In a multi-package environment without workspaces, every sub-project
maintains its own node_modules directory, leading to
duplicate dependencies and massive disk usage. npm workspaces solve this
by hoisting shared dependencies to a single root-level
node_modules folder. The root
package-lock.json file locks the versions for the entire
repository, ensuring consistent builds across all packages.
2. Automatic Symlinking
When a package inside the workspace depends on another package within
the same repository, npm automatically creates a symlink in the root
node_modules. If package A declares package
B as a dependency in its package.json, npm
resolves B locally rather than fetching it from the npm
registry. Changes made to package B are immediately
reflected in package A without manual linking steps.
3. Centralized Script Execution
Workspaces allow you to run npm lifecycle scripts (such as
test, build, or lint) across all
sub-packages simultaneously or target specific packages directly from
the root directory using CLI flags.
Setting Up an npm Workspace
To initialize a monorepo with npm workspaces, define the
workspaces property in the root package.json
file.
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}The root project is typically set to "private": true to
prevent accidental publication to the npm registry. The
workspaces array uses glob patterns to specify the
directories that contain individual packages.
A standard directory structure looks like this:
my-monorepo/
├── package.json
├── package-lock.json
├── packages/
│ ├── ui-components/
│ │ └── package.json
│ └── utils/
│ └── package.json
└── apps/
└── web-app/
└── package.json
Common Workspace Commands
Managing dependencies and executing tasks is handled through standard npm commands paired with workspace flags:
Install a dependency to a specific workspace:
npm install lodash --workspace=apps/web-appInstall a dependency to the root (shared tooling like ESLint):
npm install eslint --save-devRun a script in a specific workspace:
npm run build --workspace=packages/ui-componentsRun a script across all workspaces:
npm run test --workspaces
By integrating these features directly into the default Node.js toolchain, npm workspaces eliminate the complexity of configuring external monorepo tools for projects that only require standardized dependency management and local package linking.