This is an alpha, sneak peek of Monorepo Maestros. For this iteration, I'm getting all of my thoughts down. In the future, we'll have better information architecture, graphics, and other awesomeness. Your feedback is welcome!

The basics

Before we get our hands dirty, let's take a second to get familiar with the mental model of a monorepo.

One repo to rule them all

In a monorepo, we're building one repository that handles all of our applications. However, this doesn't mean we want to think of our monorepo as one giant chunk of code.

Workspaces are like mini-projects

Every application and package that you build will be in its own workspace. The key is to think of your workspaces as independent, small projects in your repository whose purpose is to straightforwardly share code to other parts of your repository. This includes dependencies and source code that is required to develop and build that workspace.

In this way, you'll start thinking of your monorepo in self-contained slices that expose an API to the outside world. This should sound familiar if you're used to writing modular code. In a monorepo, we can encourage ourselves and our teammates to use the workspace boundary as a module boundary.

Tasks in a workspace should be self-contained

As we start thinking about packages this way, a great way to check if you are handling your workspaces correctly is to see if the tasks of the workspace run without reaching for code outside the workspace. Any code that is used in the workspace should come from a dependency installed in that workspace.

Using ESLint as an example

A common example I've seen of breaking this rule (where we shouldn't be) is with ESLint. An incorrect ESLint setup would place a eslintrc.json at the root of the project and run a eslint . from the root as well.

This comes with problems:

You'll learn how to improve your type checking scripts in the TypeScript section but let's go back to continuing to refine our mental model.

Thinking in dependency graphs

When we install one workspace into another, we've created a relationship between those workspaces from "producer" to "consumer." For example, a workspace containing a UI package produces and exposes an API surface to a "consumer" web application.

An image showing an arrow between two boxes. The first box has the label 'UI package' with an arrow pointing to a 'web application'. The line is labeled with 'pnpm i' to demonstrate that we install the UI package into the web application.

But, remember that these are Typescript applications so we do need to have some configuration to handle our TypeScript. We'll create a common TypeScript configuration that we'll install into our workspaces to set those up.

The same image as the previous but we've added a box with 'ESLint configuration package.' This package is installed into each application.

We've now earned ourselves a mental model where we can think about our monorepo graphically because we created clearly defined boundaries. We can think of our repository with a "direction", working from the bottom of our dependency tree (configurations) all the way up to the top (applications).

If you're learning this for the first time...

These ideas may sound a little abstract - but that's okay! For the rest of this reference, we will be more hands-on with some code and see how these concepts work in action.

Let's now take a look at setting the scene for your repository in your repository's root.