How to setup a monorepo for react with yarn workspaces

hero-image

In this post I will show you some of the benefits of using a multi-package repository (“a monorepo”) to structure your large scale frontend projects. I will guide you through a simple example and show you some best practices. We will use Javascript and React in this project, still concepts should be applicable to other project types as well.

I’d love to hear how you do things and what you’ve found works well down in the comments.

Prerequisites

Before reading you should have some fundamental knowledge of the following technologies:

  1. Node.js and npm

  2. React

  3. Yarn

  4. Git

  5. nwb

The Structure

At first we will need a solid directory structure. We will host multiple packages in different directories. Let’s give our root directory the following name: ACME. It’s the client we’re working for. Our client has two websites: one company website and a shop. For each of these projects we create a subdirectory. All packages will live inside a “packages” directory.

Both shop and website follow the same corporate identity and thus will share one component or another (eg. different Buttons, Selects, Text-Fields and so on). Let’s create a “common” directory for these. Components in this directory will be available for all packages.

The same applies for services, eg. a content service, which connects to a headless CMS. Or think of a twitter service that fetches ACME`s latest tweets (or lame jokes like in the example).

Anyway, we will end up with a folder structure like this:

[acme]
├── package.json
└── packages
    ├── common
    ├── services
    ├── shop
    └── website

A closer look reveals that we have to deal with two different types of packages: libraries (common, services) and apps (shop, website).

We will use create-react-app to initialize the applications and nwb for the libraries. Both projects have excellent documentation, so I won’t cover the installation process in detail.

After you are done with the installation of all four packages, go ahead and delete all node_modules directories. Why? Well, we’ll learn that in the next part.

The Setup

To setup our packages architecture we’ll use a Yarn feature that’s available from Version 1.0: Workspaces. This feature comes along with some nice benefits. Apart from removing redundancy, it allows you to link packages, which means your packages can depend on each other.

We will use yarn init within the root directory to create a package.json file. Add the following to your freshly created file. We’ll call this directory the “workspace root”. Keep in mind that yarn requires you to set private : true .

[ACME/package.json]

{
  **"name": "@acme/core",**
  "version": "1.0.0",
  "license": "MIT",
  **"private": true,**
  "**workspaces**": [
    "**packages/***"
  ]
}

You define your packages within the workspaces: [] array. In the example above we tell Yarn that all our packages will live under packages/*. Setting the wildcard makes life easy and fun (at least in this scenario).

Next go ahead and give you’re packages a name and a version number. It’s good practice to prepend a prefix. This opens up a namespace to avoid conflicts with other packages you might want to use.

[ACME/packages/**common**/package.json]

{
  "name": "**@acme/common**",
  "version": "**0.1.0**",
  "private": true,
  (...)
}

[ACME/packages/**services**/package.json]

{
  "name": "**@acme/services**",
  "version": "**0.1.0**",
  "private": true,
  (...)
}

Now that we have all our packages named, we can go on and add them as dependencies in packages where we want to use them. The package @acme/shop for instance depends on both @acme/common and @acme/services and thus has them listed as dependencies in its package.json file.

[ACME/packages/**shop**/package.json]

{
  "name": "**@acme/shop**",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "**@acme/common**": "**0.1.0**",
    "**@acme/services**": "**0.1.0**",
    ...
  },
  ...
}

The same applies for ACME’s website package.

[ACME/packages/**website**/package.json]

{
  "name": "**@acme/website**",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "**@acme/common**": "**0.1.0**",
    "**@acme/services**": "**0.1.0**",
    ...
  },
  ...
}

Having all in place we can install dependencies by calling yarn install from within the workspace root. The reason for removing the node_modules directories in the first place was that we wanted to allow Yarn to install all project dependencies together. Yarn uses a hoisting scheme to reduce redundancy. In this process all dependent packages are flattened into a global location (our workspace root).

The Example

It’s time to get your hands dirty. Feel free to clone the project from Github and install its dependencies.

git clone [https://github.com/thepeaklab/acme-monorepo.git](https://github.com/thepeaklab/acme-monorepo.git)
cd ./acme-monorepo
yarn install

You will find the following in this repository:

  • the monorepo structure described above

  • a “HelloWorld”-component in the “common” library

  • one service that fetches jokes from David Katz random joke api

Don’t be afraid to fiddle around with it.

Start with running yarn run website. This will build both packages common and services and fire up the website’s development-server. If you want to start the shop call yarn run shop. That’s it.

You should end up with something like this …

… two completely independent projects able to share components and services from the same repository.

Both projects are almost identical and self-explanatory. Both have an instance of “HelloWorld” and both display data fetched from our service. The only difference is the value of the “name” property passed to “HelloWorld”.

packages/shop/src/App.jspackages/shop/src/App.js

Of course you don’t have to stop here. Imagine shared state reducers, reused business logic, maybe an independent storybook package to display your components and so on … possibilities of keeping your code DRY are endless. You will end up with a maintainable structure that encourages reusability.