Screenshot displaying the folder structure of packages at the root level.

Npm workspaces using TypeScript

If having a monorepo wasn’t complicated enough, trying to share code between multiple packages inside the monorepo usually becomes a battle that ends on “let’s just duplicate the code”. Workspaces —introduced into npm starting from version 7— let us solve this without manually running npm link.

A common setup is a repo that includes both a client and a server app:

If we wanted to share code, we could create a directory at the same level as both client and server (but this could end with issues importing files from outside the app’s directory), or we find a complex solution, or duplicate the code, or use npm link and create separate packages inside this same repo —but don’t ask me to set this up for a teammate or external contributor—.

TL;DR;

  1. Use the -w npm flag to init a new workspace: npm init -w <path_to_workspace>.
  2. Add a build script to the workspace using TypeScript: "build": "tsc index.ts ...".
  3. Run npm install at the root directory to make it available to every app inside the monorepo —even to other workspaces—.
  4. Lastly, make sure to run the workspaces’ build script for all workspaces when the root app is built: "build": "npm run build --workspaces --if-present && …".

Like this:

Solution

With workspaces, this is streamlined. Let’s say we have an error library that we would like to share with both the client and server:

We can directly run npm commands in the context of workspaces by using the -w flag (we can do this for every npm command):

This does two things

1. First, it adds a new entry in the root package.json for this new workspace:

2. Second, it creates the ./packages/errors directory and initializes a package.json:

A few things to keep in mind;

  • @ourapp” scope can be anything but try to use something unique, so it doesn’t collide with external modules, such as @ourapp-packages or @ourapp-common. This has the side benefit of letting your developers know that it is a package from your app and not an external library.
  • I replaced main property with module because I use bundlers for the client and server apps. No need to assume it will be used without it like other external libraries.
  • 🚨 I added the build script which uses TypeScript instead of a regular bundler to compile the code. This also helps with declarations, which are essential for shared code.
  • I added types: "dist/index.d.ts" for obvious reasons.
  • I added private: true because this is a privately shared package.

Workspace dependencies

To install dependencies for workspaces, you can navigate into the directory and install it the regular way npm install <dep> or use the npm -w flag:

Building

When building the full product, we build the workspaces first by prepending a command to the build script inside the root package.json:

The key is that we are using TypeScript to build each package and we use the --if-present flag to tell npm to build the package if there is a build script before we build our own:

We can do the same for testing.

Installing workspaces

Run npm install at the root level and npm will make the workspaces available for use without having to manually install them for each app as a dependency.

Usage

Your workspace can look something like this:

Your server or client now would look like this without having to install the packages as dependencies:

--

--

Staff Sofware Architect 2 at PayPal, M.S. in Computer Science w/specialization in Computing Systems, B.Eng. in Computer Software.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Pablo Garcia

Staff Sofware Architect 2 at PayPal, M.S. in Computer Science w/specialization in Computing Systems, B.Eng. in Computer Software.