linkedin twitter github linklocationarrow

JavaScript monorepos with Lerna and Yarn Workspaces

What is a monorepo ?

The monorepo term is a compound word between "mono", from Ancient Greek "mรณnos", that means "single" and "repo" as a shorthand of "repository".

A monorepo is a strategy for storing code and projects in a single repository.

What are they useful for ?

โ™ป๏ธ Reusing isolated pieces of code

Monorepos allow you to reuse packages and code from another modules while keeping them independent and isolated. This is particularly useful when you have a ton of code that you're constantly repeating on different projects.

๐Ÿงฐ Simplifying dependencies management

Dependencies are hoisted to the root level of the project, that means you can share dependencies across all the packages that you have in your monorepo. This reduces the overhead from updating and managing multiple versions of the same dependency.

๐Ÿ›  Refactoring cross-project changes

Making cross-repo changes within different repositories is painful. Typically involves manual coordination between teams and repos. For example let's say you have an API that is used by many clients and you want to make a breaking change into the contract. It's not trivial to apply the update to all the clients and then coordinate the deploy of the projects and so on. With a monorepo it's easier since everything is contained in a single unit.

Before considering to implement a monorepo architecture, make sure you have the problems that this concept solves โš ๏ธ. There's no need to overengineer a project. Remember keep it simple โœจ

The tools

  • ๐Ÿ‰ Lerna: The tool for managing the monorepo packages.
  • ๐Ÿ“ฆ Yarn Workspaces: Multiple packages architecture.

Now that we know what is a monorepo, the tools that we're going to use and what are they useful for, let's create a real example to see how it works.

Creating the monorepo

Setup

Let's begin creating our monorepo ๐Ÿ‘. The first thing we need to do is define the structure of the project. In this example I created two directories:

  • ๐Ÿ“ packages/: This directory will contain the isolated modules that we are going to reuse on all the applications.
  • ๐Ÿ“ applications/: This directory will contain all the applications of our monorepo.
.
โ””โ”€โ”€ src
    โ”œโ”€โ”€ applications
    โ””โ”€โ”€ packages

After that, we're going to create package.json to define the workspaces and dependencies of our monorepo.

The workspaces field is what Yarn uses to symlink our code to the node_modules in order to reuse and import the code, we'll see this later on.

Finally we install lerna as a devDependency to manage the monorepo.

{
  "private": true,
  "engines": {
    "yarn": ">=1.17.3"
  },
  "name": "monorepo-example",
  "workspaces": [
    "src/applications/*",
    "src/packages/*"
  ],
  "scripts": {},
  "devDependencies": {
    "lerna": "latest"
  }
}

Now, let's define how Lerna is going to manage our monorepo in a lerna.json configuration file.

  • packages: The directories that we defined as workspaces in the package.json.
  • npmClient: The client used to run the commands.
  • useWorkspaces: This flag tells lerna that we're going to use yarn workspaces.
{
  "lerna": "latest",
  "packages": [
    "src/applications/*",
    "src/packages/*"
  ],
  "version": "1.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}

We finished our setup ๐Ÿ™Œ! Let's add some simple code to see how we can manage and reuse packages on our monorepo.

Creating packages

A package inside our monorepo context, is an isolated and reusable piece of code. That means, every time we want to create a new package, we're going to create a new independent directory.

.
โ””โ”€โ”€ packages
    โ””โ”€โ”€ sayHello
        โ”œโ”€โ”€ index.js
        โ””โ”€โ”€ package.json

Each package needs to have a package.json with the name and version fields defined. This is important because this describes how we're going to import and use this package on the code base. You can also have dependencies in your package if you need them. On this example I'm writing a simple package called sayHello.

{
  "name": "@packages/sayHello",
  "version": "1.0.0",
}

Think of every directory inside the packages/ folder as an isolated module, with his own tests, dependencies and code.

const sayHello = (name) => {
  console.log(`Hello ${name} ๐Ÿ‘‹๐Ÿผ`)

  return name
}

module.exports = sayHello
Using packages

This was pretty simple right? Now let's say that we have an application that it's called cli. In order to use sayHello package we should add it as a dependency on the package.json file. To do that we have a fancy yarn command ๐ŸŽ‰

yarn workspace @applications/cli add @packages/sayHello@1.0.0

Now from our cli application we can import and use the package! ๐Ÿ’ฏ

const sayHello = require('@packages/sayHello')

sayHello('Carlos')

Finally, we run our cli application from the command line using Lerna ๐Ÿš€

monorepo-workspaces

You can find the example explained on the post on this GitHub repository ๐Ÿ‘€. I know this was pretty simple, but there are a ton of things you can do with monorepos! For example you can share react components in different applications while keeping them isolated. But take a look below ๐Ÿ‘‡ to see monorepos on big open source projects!

Opensource monorepo projects

Here's a list of well known open source projects that are using the monorepo architecture: