oli's profile picture
Article3.2 minute read

Better dev environments with npm workspaces

As of version 7 npm now includes a feature called "workspaces". This is designed to help you manage multiple separate packages within a single project.

For example you may have a React frontend and an Express backend for your app that you want to manage as a single repository on GitHub. However this makes development awkward, as you constantly have to cd into the right directory to install dependencies or run npm scripts.

npm setup

To use workspaces you need to be on the latest version of npm. Although the feature was added in npm 7 some of the best parts weren't available until later (e.g. installing dependencies came in v7.14.0).

If you originally installed Node and npm with a management tool like Volta then you can get the latest version of npm by running volta install npm in your terminal. If not you can use npm to upgrade npm (🤯) by running npm install -g npm.

Project setup

Let's imagine you have a minimal project set up with two sub-directories for your client and server:

my-project/
  client/
    index.js
    package.json
  server/
    index.js
    package.json

To use workspaces you need to create a package.json at the root of your project with the "workspaces" config:

{
  "name": "my-project",
  "workspaces": ["client", "server"]
}

This tells npm that the client and server directories should be managed as workspaces.

Using workspaces

Most npm commands can now have workspace-related options added to make them run against just one (or all) of your workspaces. To run a command against a single workspace you can append --workspace=client. To run a command against all workspaces you can append --workspaces (note the s).

All the following commands are run from the root directory. No cding around required!

Installing packages

To install all packages for all workspaces you can run:

npm install --workspaces

This is very useful when you first clone a project, to immediately get all the dependencies installed so you can run everything.

Note: npm will create a single package-lock.json and node_modules at the root of your repo containing all the dependencies. This can be more efficient as one combined node_modules is smaller (because there's less duplication).

To install a package into a single workspace you can run:

npm install express --workspace=server

Running scripts

You can run npm scripts defined in your workspaces' package.json files. Read more in the npm run-script docs.

To run a script from a single workspace:

npm run test --workspace=client

Note this requires that client has a script named "test" defined in its package.json.

To run the same script in all workspaces:

npm run test --workspaces

Missing scripts

If any workspace is missing a script you'll get an error trying to use --workspaces. Sometimes you just want to run all the scripts if they exist, and you don't care if they don't. There's an option for this:

npm run test --workspaces --if-present

Now only those test scripts that actually exist will be run.

Running in parallel

Unfortunately npm runs the scripts in series, in the order the workspaces were defined in the top-level package.json (as far as I can tell, this isn't documented). This means you can't use it for long-running processes like dev servers, since the first script will never finish (and so the second script will never run).

Until npm implements something like Yarn workspaces's foreach --parallel you'll have to workaround this by writing your own script.

In Mac/Linux environments you can run tasks in parallel using the & operator. So you could make an npm script in your top-level package.json to run multiple workspace-level scripts:

{
  "scripts": {
    "dev": "npm run dev --workspace=client & npm run dev --workspace=server"
  }
}

This will run both scripts in the same terminal, which means all your logs will be mixed together. I also have no idea if it will work with tools that hijack the whole terminal (like Create React App). So in these cases you probably still want to just open two separate terminal tabs/windows.