Monorepo setup with Npm Workspaces & Lerna
Tags: monorepo, npm, lerna, react
Published: 2023-04-30 09:59:25

Monorepos lets you manage multiple repositories into one single repository but managing and publishing them can be very tedious as the repositories grow.  
Lerna is a powerful tool that can be used to manage and publish monorepos in Javascript/Typescript projects.

> Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository.

In this article, we will see how we can set up a monorepo with Lerna and Npm workspaces.

_If you are interested in setting up a monorepo with_ **_Vite, Pnpm, and Typescript_** _you can check out my previous blog_ [_here_](https://dev.to/imvedanshmehra/setting-up-a-monorepo-with-vite-typescript-and-pnpm-workspaces-19ak-temp-slug-6616308)_._

* * *

## Why use Lerna?

> To use monorepos well, you need a good monorepo tool, and Lerna is exactly that. Lerna is fast, widely used, and battle tested.

Monorepos can have tens if not hundreds of packages, and managing all of them together can become very difficult. Lerna can rescue us in this situation by doing the heavy lifting of managing the tasks in different packages, versioning, code deployment, etc.

Lerna has some great in-built features which make managing monorepos surprisingly easier.

- **Running tasks within multiple packages** : Lerna gives us an out-of-the-box solution to run tasks inside multiple packages together using `lerna run` command. For example, Let's say you have 3 packages inside your monorepo and you want to run the `test` script for all of them, with Lerna all you have to do is just run `npx lerna run test` command from the root of your project and it will run the `test` script inside each of your packages (You must have defined the `test` script in package.json for all the projects respectively in order to make this work properly).

- **Visualize the workspace** : When the workspace becomes very huge it is common for a new developer to face difficulty in understanding the hierarchy of the workspace and understand how different packages are linked together. Lerna uses `Nx` to help visualize the workspace graph. Simply run `npx nx graph` and it will launch a project visualization graph on your browser.

You can check the complete list of Lerna features [here](https://lerna.js.org/docs/introduction#why-lerna).

## Who uses Lerna?

Lerna is being used by many different teams including but not limited to:

- Jest

- Create React App

- Nx

- NestJS

## Project Setup

### Prerequisites

To follow along, you must have `Node` installed in your system which I am assuming that you would already have. Other than that, some basic knowledge of Javascript/Typescript and React will help in understanding the code that we are going to write.

### Project Overview

For the sake of this article, our monorepo will have two packages:

1. **common** : A shared library that can be used by multiple packages.

2. **client** : A basic React app that will consume the `common` package and will render the components on UI.

### Getting Started

Let's start off by creating a new directory at your desired location.

```
mkdir npm-lerna-monorepo

```

Then, move inside your newly created directory and initialize a Lerna workspace.

```
# npm-lerna-monorepo
npx lerna init

```

This command will do the following things:

- Create a `package.json` file if it is not already present and add `lerna` as a dev dependency.

- Create and configure a new `lerna.json` file.

- Configure npm workspaces inside `package.json`.

- Create a `packages` directory that will contain all our monorepo packages.

```
 lerna.json
 .gitignore
 package.json
 packages

```

Now, in order to use Typescript in our project, we need to install a few dependencies at the root level.

```
npm i typescript @types/node -D

```

Once the above dependencies are installed, create a common `tsconfig.json` file that will be extended by all the other packages of our app.

```
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": false,
    "noImplicitAny": false,
    "removeComments": true,
    "noLib": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es6",
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "lib": ["es6"]
  },
  "exclude": ["node_modules"]
}

```

Now we can start creating our packages, to do that let's go inside the `packages` directory and create the `common` package first.

```
npm create vite@latest common --template react-ts

```

We should now have a `common` package inside our `packages` directory, now let's move inside the `common` directory and run `npm install` to install all the required dependencies.

```
# npm-lerna-monorepo/packages/common
npm install

```

In order to use the base Typescript configuration that we've defined at the root level, we need to add the following snippet at the beginning of our `tsconfig.json` inside the common package.

```
// npm-lerna-monorepo/packages/common/tsconfig.json
{
  "extends": "../../tsconfig.json",
  ...
}

```

We will now go ahead and create a Button component and will export it so that it can be used by the other packages in our monorepo.

```
// npm-lerna-monorepo/packages/common/src/components/Button.tsx

interface ButtonProps {
    buttonText: string;
}

const Button = ({ buttonText }: ButtonProps) => {
    return (
        <button>{buttonText}</button>
    )
}

export default Button;

```

Now export this Button component from `main.tsx` file which will be our entry point.

```
// npm-lerna-monorepo/packages/common/src/main.tsx
import Button from "./components/Button"

export { Button }

```

We will set `main.tsx` as our entry point by setting the `main` property in `package.json`

Also, as per the common workspace naming convention of monorepos we'll change the name of the `common` package to `@npm-lerna-monorepo/common`

```
// npm-lerna-monorepo/packages/common/package.json
{
  "name": "@npm-lerna-monorepo/common", // Change the package name
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "main": "./src/main.tsx", // Define the entrypoint file
  ...
}

```

We should now be able to import components from the `common` package very easily after linking it with the `client` package.

Let's now create our `client` package, for that we'll move inside the packages directory and run the following command:

```
# npm-lerna-monorepo/packages
npm create vite@latest client --template react-ts

```

Our project structure after running the above command should look somewhat like this:

![project-structure](https://cdn.hashnode.com/res/hashnode/image/upload/v1682234310655/91401e0b-e1ec-47c6-83ab-aa0afe129ac8.png)

Let's update the `tsconfig.json` of the client package now to extend the base Typescript configuration that we've defined at the root level.

To do that add the following line of code at the beginning of `client/tsconfig.json` :

```
// npm-lerna-monorepo/packages/client/tsconfig.json
{
  "extends": "../../tsconfig.json",
  ...
}

```

Now add the `common` package as one of the dependencies for the `client` package, we can add the common package using the name that we've defined in its `package.json` file.

```
// npm-lerna-monorepo/packages/client/package.json
{
  "name": "@npm-lerna-monorepo/client", // Updated the name
  ...
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@npm-lerna-monorepo/common": "*" // Added common package
    ...
  },

```

We've also changed the name of the `client` package according to the monorepos workspace naming convention.

Let's link both of our packages together by running `npm install` from the root level.

```
# npm-lerna-monorepo
npm install

```

This will link both packages together and now we can easily import the Button component from the `common` package and use it in our `client` package.

```
// npm-lerna-monorepo/packages/client/src/App.tsx
import { Button } from "@npm-lerna-monorepo/common"

const App = () => {
  return <Button buttonText="Hello world" />
}

export default App;

```

Run the dev server for `client` package now.

```
# npm-lerna-monorepo/packages/client
npm run dev

```

Open `localhost:5173` on your favorite web browser and you should see a button rendered on the UI.

So we have successfully setup up our monorepo using Npm workspaces and Lerna and while you are in our local environment, any changes you will make to the `common` package will immediately reflect on the `client`.

Hooray 🎉

You can find the complete code of the setup [here](https://github.com/imvedanshmehra/npm-lerna-monorepo).

## Conclusion

In this article, we have seen how we can set up and manage a monorepo with Lerna and Npm workspaces.

Though Lerna is a great tool, you don't need to include it in all of your monorepos it's mainly suitable for large applications where you have multiple packages that you need to manage simultaneously.

While this article was majorly focused on the setup part, if you are interested in reading more about how we can use Lerna to build and publish our monorepos please let me know in the comments.

I hope this blog could a little value to you. If you liked it please share it with those who might find it helpful and show some to the article.

You can also follow me on [Twitter](https://twitter.com/imvedanshmehra) where I post more content on Javascript, React, Software Engineering, and other stuff.