npm workspaces let you manage multiple npm packages within a single repository, simplifying dependency management and local development.
Here’s a monorepo setup using npm workspaces in action:
Imagine you have a project with a shared UI library and several applications that use it.
my-monorepo/
├── packages/
│ ├── ui-library/
│ │ ├── index.js
│ │ ├── package.json
│ ├── app-one/
│ │ ├── index.js
│ │ ├── package.json
│ ├── app-two/
│ │ ├── index.js
│ │ ├── package.json
├── package.json
The top-level package.json is where you enable workspaces:
{
"name": "my-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
]
}
The workspaces array tells npm to look for packages in the packages/ directory. private: true is crucial because the root package itself isn’t meant to be published.
Now, let’s look at packages/ui-library/package.json:
{
"name": "@my-org/ui-library",
"version": "1.0.0",
"description": "A shared UI component library",
"main": "index.js"
}
And packages/app-one/package.json:
{
"name": "app-one",
"version": "1.0.0",
"description": "The first application",
"main": "index.js",
"dependencies": {
"@my-org/ui-library": "workspace:*"
}
}
Notice how app-one depends on @my-org/ui-library using "workspace:*". This special version specifier tells npm to use the local version of @my-org/ui-library from the same monorepo.
After creating this structure, you run npm install in the root of my-monorepo/. npm will:
- Read the
workspacesconfiguration. - Symlink the
ui-librarypackage into thenode_modulesdirectory ofapp-oneandapp-two. - Ensure that
@my-org/ui-libraryis available toapp-oneandapp-twowithout needing to publish it to a registry.
This solves the problem of managing dependencies between internal packages. Instead of publishing and consuming beta versions or using complex file paths, you can treat them as regular npm dependencies that are automatically linked. Internally, npm makes this work by creating symlinks from the dependent package’s node_modules folder to the actual package directory within the packages/ folder. This means changes you make in ui-library are immediately reflected in app-one and app-two without any rebuilding or re-publishing steps.
The primary benefit is simplified dependency management and development speed. You can install, link, and manage dependencies across packages seamlessly. When you run npm install at the root, npm hoists common dependencies to the root node_modules folder to reduce duplication, further optimizing install times and disk space. For instance, if both app-one and app-two depend on react, a single react installation might appear at my-monorepo/node_modules/react instead of being duplicated in each app’s node_modules.
When you run npm install with workspaces, npm performs a topological sort of your packages based on their dependencies. This ensures that packages are linked in the correct order, preventing issues where a package might try to use a dependency that hasn’t been linked yet. This is particularly important for packages that have internal dependencies within the monorepo.
A subtle but powerful aspect of npm workspaces is how they handle transitive dependencies. If app-one depends on ui-library, and ui-library depends on lodash, npm will ensure lodash is available to ui-library and potentially hoist it to the root node_modules if it’s a common dependency. You don’t need to explicitly add lodash to app-one’s dependencies.
The next hurdle you’ll face is managing scripts and builds across multiple packages in a coordinated way.