A monorepo, contrary to what its name suggests, is not a bonobo jumping from branch to branch of our repository. A monorepo is a repository that hosts multiple projects.
Hosting several projects in a single repository may seem like a big mess, but it offers some advantages. Obviously, we are talking about related projects, it would not make much sense to put in the same repository projects for different clients, for example.

Advantages
Suppose we start a new project. It must include a mobile app created with React Native, a web app with Vue and a desktop app created with Electron. In addition, we want to have a mocks server to be able to work locally, regardless of whether the backend is to be developed. As you can already guess, most of the code of the applications will be in many cases the same, only the visual presentation with the components, routers, etc, of each framework will be different. On the other hand there are many dependencies that would also be repeated in several projects.
Working in separate repositories, it is more than likely that we will repeat a lot of code between the three applications, in addition to having to set up a mocks server for each one, which is anything but efficient.
Tools for managing a monorepo
Lerna
Lerna is a Javascript/Typescript oriented monorepo tool, used among others by React or Jest. Among other things it allows us to:
- Execute tasks, either for the entire repository or for one or several packages.
- Caching of the result of the tasks. We can, for example, do a build and then make changes to a specific package. When redoing the build, will only be made from the modified package, caching the rest.
- Versioning and publishing. Lerna allows us to launch and publish releases of our packages, keeping the semantic versioning separate for each of the packages.
Yarn Workspaces
Yarn Workspaces is used by Lerna at a lower level. It allows us to launch a single installation of dependencies and ensures interdependencies between packages. It uses a single yarn.lock for all packages, thus ensuring better stability between them.
When we use one of our packages as a dependency on another, Yarn creates a symbolic link in node_modules to the package we depend on, always using our most up-to-date code.
Creating a monorepo with Lerna
We are going to create a React application and a pure Typescript utility application. First we will install Lerna globally:
$ npm i -g lernaTo create our monorepo with Lerna, we place ourselves in the directory of our project and launch the following command:
$ lerna init --independent --packages="packages/*" --skip-installWhat are these modifiers?
--independent. With this flag we tell Lerna to maintain a separate versioning for the packages--packages. We indicate the route where to find our packages.--skip-install. It does not perform dependency installation. By default Lerna uses npm, but we will use Yarn.
Once initialized, we install the dependencies with Yarn:
$ yarn installThis will leave us with the following directory tree:
React application
We will create our packages folder, and then, placing ourselves inside the newly created directory, we will create a React application:
$ create-react-app awesome-react --template typescriptOnce the whole create-react-app party is finished, we notice that a file has not been created. .lock in the React application directory, and that, in addition, the folder node_modules corresponding to our application has only one folder:
It is convenient to give an identifying prefix to your app in its package.json:
"name": "@awesome/awesome-react.",Our dependencies are located in the node_modules folder of the root directory and are registered in the file yarn.lock also from the root directory.
We can also see that our application is referenced in the file yarn.lock:
To launch our application we can do as usual, go to its directory and launch yarn start. But let's take advantage of Lerna's bounties.
In the file package.json directory, we can centralize the scripts of all our packages by using the command lerna exec. In this case we will create the following script to launch our application:
"scripts": {
"start:react": "lerna exec --scope @awesome/awesome-react -- yarn start"
}What is happening here? Via --scope we indicate which package is the one we are interested in, in this case @awesome/awesome-react is the name of our React application. Then, after the double dash, we indicate the command we want to launch from the package in question.
Utility package
We are going to create a utility package for use in the React application. To do this we will create a folder called utilities in the directory packages. Placing ourselves in that folder, we initialize the Typescript project. Let's first create the file package.json:
$ yarn initAfter answering a few questions we will have created our package.json that we will have to edit. First let's install Typescript:
$ yarn add -D typescriptWe edit the package.json. It will look like this:
{
"name": "@awesome/utilities",
"version": "0.1.0",
"description": "Some awesome utilities",
"main": "dist/AwesomeCounter.js",
"types": "dist/Awesomecounter.d.ts",
"license": "MIT",
"private": true,
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.2.2"
}
}- On the property
mainwe indicate the transpiled file of our utilities. - The entry
typesindicates the file path.d.tsthat will be generated when creating the build. - We create a script to make the build of our package.
In the file tsconfig.json we also have to make some modifications:
"declaration": true,
"sourceMap": true,
"outDir": "./dist", statement: truewill cause a .d.ts file to be generated.sourceMap: truewill generate a sourceMap file for debugging.outDir: "./dist"indicates dist as the directory of the generated files.
Once this is done, we create a src folder and a utility class:
// AwesomeCounter.ts
export default class AwesomeCounter {
static increase(amount: number): number {
return amount + 1;
}
}We already have a great utility that we can't live without. Software development will never be the same again.
Next we make the build. From the folder package/utilities:
$ yarn buildOr we can create a script in the file package.json of the root:
"build:utilities": "lerna exec --scope @awesome/utilities -- yarn build"$ yarn build:utilitiesThis action will generate the following file tree:
Use of the utility package
We are going to create an amazing counter application in our React app. First we will add our internal dependency in the package.json of our application as follows:
"@awesome/utilities": "*",To make our packages available to the other packages in our project, we must launch a yarn install. This will create symbolic links in the folder node_modules of the root of the project:
Once this is done, we are going to create a component in the React application. Being extraordinarily original and creative, we will call it Counter, for example. What you are going to see next does not make much sense (none, actually), but it is simply to exemplify the use of a package inside another package in a monorepo environment:
import { useState } from 'react';
import AwesomeCounter from '@awesome/utilities'
const Counter = (): JSX.Element => {
const [count, setCount] = useState<number>(0);
const increaseCounter = (): void => {
setCount(AwesomeCounter.increase(count));
};
return (
<>
<p>{count}</p>
<button onClick={increaseCounter}>Click!</button>
</>
);
};
export default Counter;The counter utility is imported in exactly the same way as any other dependency as it is available alongside the others.
We import our counter in the file App.tsx created by default (because there is no need to complicate your life):
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Counter from './components/Counter'.';
function App() {
return (
<div className='App'>
<header className='App-header'>
<img src={logo} className='App-logo' alt='logo' />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className='App-link'
href='https://reactjs.org'
target='_blank'
rel='noopener noreferrer'.'
>
Learn React
</a>
<Counter />
</header>
</div>
);
}
export default App;We launch the React application, and if everything went well, we will be able to see in our browser an amazing counter that increases every time we click on the button. Fascinating.

Versioning
Lerna allows us to automatically version our packages independently. To do this we must previously make a commit with the files that we have modified and then we execute the following command:
$ lerna versionWe will get a prompt as the following for each package affected by the changes:
$ lerna version
info cli using local version of lerna
lerna notice cli v7.3.0
lerna info versioning independent
lerna info Looking for changed packages since @awesome/awesome-react@0.1.1
? Select a new version for @awesome/awesome-react (currently 0.1.1) (Use arrow keys)
❯ Patch (0.1.2)
Minor (0.2.0)
Major (1.0.0)
Prepatch (0.1.2-alpha.0)
Preminor (0.2.0-alpha.0)
Premajor (1.0.0-alpha.0)
Custom Prerelease
Custom VersionWe had made a modification in the utilities package, on which the React application depends, so it asks us for the version of the two packages. We select the version type, for example ‘Minor’. Once we have selected the version type in both cases we are asked for confirmation.
? Select a new version for @awesome/utilities (currently 0.1.1) Minor (0.2.0)
Changes:
- @awesome/awesome-react: 0.1.1 => 0.2.0 (private)
- @awesome/utilities: 0.1.1 => 0.2.0 (private)
? Are you sure you want to create these versions? (ynH)y
lerna info execute Skipping releases
lerna info git Pushing tags....
lerna success version finishedWe can check our repository to see that a new commit and we have new labels:
Deprecated commands
In many Lerna tutorials you will see the commands lerna add, lerna link y lerna bootstrap. You should note that these commands have been removed in Lerna version 7..
References
Lerna: https://lerna.js.org/
Yarn Workspaces: https://classic.yarnpkg.com/lang/en/docs/workspaces/
Semantic Versioning: https://semver.org/
