I’ve really learned to love a good monorepo setup, a repository that contains multiple packages and/or applications.

Being able to make changes across applications or packages in 1 pull request (PR), having the option to centralize and reuse code over applications, and unifying documentation and processes greatly simplifies the daily task of finding your way across multiple projects.

A monorepo comes with its own level of complexity and prompts questions such as:

  • How do we handle dependencies?
  • How do we run scripts?
  • Where do we define build pipelines?
  • How do we publish and manage packages?
  • How do we deploy our applications?

A lot of questions.

Luckily, tooling has improved leaps and bounds over the last couple of years. Features like Yarn workspaces and tools like Lerna, NX, and Turborepo (which was acquired by Vercel) are in a pretty stable phase and can help you address those issues.

There has never been a better time to jump in than now. 

Over the course of multiple articles, I’ll guide you through the process of setting up a monorepo using mentioned tools. Let’s start with managing dependencies and sharing code in part 1.

1. Initializing a new monorepo with yarn

Create a folder called ‘monorepo-101’, ‘cd‘ into it, run ‘yarn init‘, then walk through the initialization steps.

Initialize a git repository by running ‘git init‘ and make sure you have a ‘.gitignore’ file containing (at least) the following lines.

.gitignore

.next
node_modules
yarn-error.log

This step is important if we want to add Turborepo later. Create the following folder structure.

/apps
    /admin
    /client

We will create Next.js applications in ‘admin’ and ‘client’; each application will have its own ‘package.json’, listing the dependencies.

Usually, this would resolve in multiple lock files and ‘node_modules’ folders. By enabling Yarn workspaces, Yarn moves all dependencies to the root ‘node_modules’ folder, manages a single lockfile, and deduplicates dependencies if possible.

Add the following fields.

package.json

...

"private": true,
"workspaces": [
    "apps/*",
],

...

The ‘private’ field indicates that you can’t publish this package to a registry. It’s required for Yarn workspaces to work and acts as a safeguard.

cd‘ into the ‘client’ directory and run ‘yarn init‘. Install the packages we need by running ‘yarn react react-dom next‘.

You’ll notice that the ‘node_modules’ folder remains empty (there might be a ‘.bin’ folder, but it shouldn’t contain any dependencies). When installing dependencies in a specific workspace, Yarn moves them to the root of your repository and manages the lockfile there.

You can install dependencies in the root of a workspace, too. We might want to install a dependency that is not tied to a specific workspace. Prettier is a great example.

Install prettier as a root dependency by adding the ‘-W’ flag: ‘yarn add prettier -W -D‘ (we’ve also added the -D flag because it’s also a devDependency).

Let’s verify that everything works as expected. Navigate back to the root of the project and remove all installed dependencies by running ‘rm -rf node_modules‘.

Now, run ‘yarn‘ (in the root of the repository), Yarn will go over all workspaces and install the dependencies, based on the lockfile.

Nice!

2. Set up two applications in the same monorepo

Let’s set up some applications. From this point onwards, I’m assuming you have some (very basic) knowledge of Next.js. If not, you can follow their excellent getting started tutorial.

Run ‘yarn init‘ in ‘apps/client’, walk through the steps, and create a homepage that renders a header.

apps/client/pages/index.js

const Home = () => {
  return <h2>Hello client</h2>
}
export default Home;

For convenience, let’s add a ‘dev’ script in the generated ‘package.json’. We’re also defining a port to have some control over which port links to which application.

apps/client/package.json

...

"scripts": {
    "dev": "next dev -p 3000"
},

...

You can copy ‘package.json’ and the ‘pages’ folder to the ‘admin’ workspace to create the admin application.

Make sure to edit the following items after doing so:

  • the name field in ‘package.json’ (to “admin”)
  • the copy in ‘pages/index.js’ (to “hello admin”)
  • the port in the dev script (to “3001”)

Run the applications by running ‘yarn dev‘ in both workspaces. You’ll need two terminal windows, for now; we’ll revisit this when adding Turborepo.

You should be able to visit the client application on http://localhost:3000 and the admin application on http://localhost:3001.

Yarn can also help you with sharing code (as a package) over applications.

Let’s introduce a UI library to create some visual consistency. Create a ‘packages’ folder in the root of your repository, and add it to the workspaces array.

package.json

...

"workspaces": [
    ...
    "packages/*",
    ...
],

...

Create a ‘ui’ workspace (folder) in the ‘packages’ folder and run ‘yarn init‘ there.

/packages
    /ui

Let’s kick off our UI library with a simple Header component.

packages/ui/Header.js

const Header = ({ children }) => {
  return <h2>hello {children}</h2>;
};

export { Header };

Export all named exports from our Header module in an index file.

packages/ui/index.js

export * from 'Header'

And, finally, edit ‘package.json’ to link everything up.

packages/ui/package.json

...

"main": "index.js"

...

3. Create a package by transpiling modules

We’ll be using next-transpile-modules to avoid having a Babel setup in our packages. If we want to publish this package to a registry (NPM) later, we might have to reconsider this (which we will when adding Lerna).

Add ‘next-transpile-modules’ as a devDependency in BOTH workspaces running.

​​​​​​​yarn add -D next-transpile-modules

Create a Next.js configuration file in both applications, and configure ‘next-transpile-modules’ to transpile the ‘ui’ package when needed.

packages/client/next.config.js

packages/admin/next.config.js

const withTranspileModules = require('next-transpile-modules')(['ui']);

module.exports = withTranspileModules({
  reactStrictMode: true,
});

Run yarn in the root of our repository to link everything up, and you should be good to go. Now we can use this package in our applications by referring to it as ‘ui’.

Finally, import the Header component and use it on our applications.

apps/client/pages/index.js

import { Header } from 'ui';

const Home = () => {
  return <Header>client</Header>
}

export default Home;

Use the same component in our admin application. When making changes to the component, both applications will refresh.

You’ve successfully created a monorepo with 2 apps and a package. In the next article, we’ll create build pipelines and enable caching by adding Turborepo. Here’s some confetti to celebrate.