Create a monorepo with npm workspaces

npm 7 introduced the concept of workspaces to facilitate the management of monorepos. Here we will explore how to create one, manage local and external dependencies, and more.

There are two things to keep in mind when working with npm workspaces:

  1. There’s only one node_modules folder located at the root of the repo.
  2. The installation of npm packages should be done at the root of the repo and using the --workspace (or -w) argument when needed (we will see more about it down below).

Here’s the folder structure of the monorepo we are going to work with in this blog post:

Folder structure
- package.json
- packages/
	- utils/
		- package.json
	- app1/
		- package.json
	- app2/
		- package.json

Create the main package.json

First of all, let’s create a folder for the monorepo and create the main package.json

Terminal
mkdir monorepo
cd monorepo
npm init

Create a workspace

Then, let’s create our first workspace, utils, which will be shared in both apps. The --workspace (or -w) argument specifies the path to the workspace.

Terminal
npm init -w packages/utils

You can create a scoped package inside a workspace too using the --scope argument:

npm init -w packages/utils --scope frago12

The command above does a few things:

Terminal
- pacakge.json
- packages/
	- utils/
		- package.json
monorepo/package.json
{
  "name": "monorepo",
  "workspaces": [
    "packages/utils"
  ],
	...
}

You can also symlink the local packages (workspaces) to the node_modules folder by executing npm install in the root of the monorepo.

Install external dependencies

Once we have a workspace created, we can install core dependencies (available for all workspaces) or worskpace-specific dependencies (available only for a specific workspace).

As mentioned above, all the dependencies must be installed from the root folder.

Core dependencies

Just npm install them from the root folder. It will add the dependency to the main package.json file located at the root of the monorepo.

Terminal
npm install -D prettier
monorepo/package.json
{
  "name": "monorepo",
	"devDependendencies": {
		"prettier": "...",
	},
  ...
}

Worskpace-specific dependencies

Same as the core dependencies, but you need to specify the workspace (-w) where you want to install the dependency.

Terminal
npm install date-fns -w @frago12/utils

This will add the date-fns dependency to the utils/package.json file.

monorepo/packages/utils/package.json
{
  "name": "@frago12/utils",
  "dependencies": {
    "date-fns": "..."
  }
}

Reference a local package

Let’s say at this point we have created a couple more workspaces, @frago12/app1 and @frago12/app2. And, we want to install the package we previously created, @frago12/utils , in one of them.

A local package can be installed in the same way external packages are.

Terminal
npm install @frago12/utils -w @frago12/app1

The command above will add the dependency in the package.json file of the specified workspace.

monorepo/packages/app1/package.json
{
  "name": "@frago12/app1",
  "despendencies": {
    "@frago12/utils": "1.0.0"
  }
}

That means we can import our local package just like any other dependency inside the app.

monorepo/packages/app1/src/index.js
import { something } from "@frago12/utils"
...

npm will try to resolve local pacakges first. If the package is not found it will try to resolve them from the npm registry. Some versions of npm could have a buggy behavior related to this. See the found issues.

Execute npm commands

One of the main advantages of having a monorepo is that you can execute npm commands in multiple workspaces simultaneously, for example, upgrading the version of an npm dependency in all workspaces:

Terminal
npm install react@18 -ws

Or only in specific workspaces:

Terminal
npm install react@18 -w @frago12/app1 -w @frago12/app2

The same strategy works for executing npm commands:

Terminal
npm run build -ws --if-present

Notice the —if-present flag. It will execute the build command in all workspaces where that script exists.

Found issues

While working with npm workspaces, I found an issue with specific versions of npm where it no longer resolves the local packages first. Fortunately, [email protected] fixes the problem. See BUG ^7.20.3 no longer resolves local package first on install (workspaces) · Issue #3637 · npm/cli · GitHub