Approaches to code quality and structure are somewhat similar to a river that gets fed by an ocean. To better illuminate this, think of the larger amount of ideas and philosophies on the matter. There is a large number of different ideas that populate the space and are available for critique and promotion to industry standards. The overall group of ideas here is the ocean, and the industry standard is the river.

Moving into the river, there is a growing sense that there will be a new idea flowing through. Monorepos. Over the past year or so I believe, this approach has grown in popularity. So it’s only right to take some time to look at what it is and figure out if it makes sense in any capacity.

Defining Monorepo

For anyone that has been around in the industry for a while, you may confuse the Monorepo approach with another, more dreaded pattern. This pattern is the Monolith. Keeping brief, a Monolith approach is having an all-encompassing repository where all the code lives in one platform. That is not what a Monorepo pattern is.

A Monorepo is a pattern where your entire platform lives in one repository, similar to a Monolith pattern. Where things begin to differ is that in a Monorepo the platform is split into several internal applications. Taking it into the JS world, think of an overarching parent directory with several internal project directories. Each has their own package.json, build scripts, and anything an application would need.

So in short, a Monorepo is a repository with a collection of separate applications that make up the platform you’re building. In fact, you could say it’s extremely loosely similar to microservices in concept only.

Understanding Under The Hood

So for setting things up, we’ll focus on doing this in JS with yarn. A primary reason for approaching with yarn is because it’s my personal preference for package management in JS. Outside of preference, yarn is also what is used at the lower level of Lerna, when possible, which is viewed as the industry standard at this point for Monorepo setup. So when in doubt, go with the lower level because it’ll give you the most insight into the entire process.

With that in mind, we’re going to define the actual technology, and associated terminology, behind what makes this possible.

Symlinking

Symlinking is a technology that’s been around for quite some time. It is the practice of linking a local directory as a package in your project. So let’s say you have a project you’re working on at ./path/to/project/x. Now inside this project, you have a sibling project u that is a dependency package of x. To avoid having to repetitively rebuild and deploy u to give access to the latest to x, we can just use a symlink to reference the local instance instead.

This is done with commands you’ve probably already seen; npm link or yarn link to be exact.

Workspaces

Workspaces is an area defined in the package.json file at the root level of your Monorepo. Easy enough to understand, this property value is an array of strings of same-level directory names that you’d like to have “symlinked” into your Monorepo architecture.

Workspace Ranges

This is something available in yarn that allows you to better specify your desire to use the local version of your Monorepo package dependencies, instead of allowing yarn to fall back to looking for a remote package.

Setting Things up

Folder Structure

.
├── index.ts
├── packages/
|    ├── common/
|    ├── client/
|    ├── server/
├── package.json

Above you’ll see a simple folder breakdown of what you could expect in your typical Monorepo setup. The package.json file at the root acts as the defining source for the parent repository. This is also where we define our Workspaces.

Root Package JSON

./package.json

{
  .....
  .....
  "workspaces":[
    "packages/**"
  ]
}

As you can see, we define the association of our subdirectory packages/ to the parent project. So now yarn will have these directories inside packages/ as a possible source for dependencies when specified inside the repository.

Packages’ Package JSON

./packages/common/package.json

{
  "name": "@projectName/common",
  "version": "1.0.0",
  ....,
  ....,
}

./packages/client/package.json

{
  "name": "@projectName/client",
  "version": "1.0.0",
  ....,
  ....,
  "dependencies": {
    "@projectName/common": "1.0.0",
    ....,
    ....,
  }
}

./packages/server/package.json

{
  "name": "@projectName/client",
  "version": "1.0.0",
  ....,
  ....,
  "dependencies": {
    "@projectName/common": "workspace:*",
    ....,
    ....,
  }
}

As you can see from the code above, everything is pretty much what you’re already accustomed to seeing. However, there is a slight difference between how client and server are declaring common as package dependency.

Looking into it, the client repo is using a specified version number and the server repo is using workspace:*. In the client, we’re looking for a particular version. If not possibly found inside the Monorepo, then yarn will begin to look online. In reality tho, why have a Monorepo if you’re just going to look for remote resources?

The declaration in our server repo solves this. The workspace:* definition ensures that your repo will always resolve to the local version of your package, in this case, our common package.

In Closing

Monorepos isn’t a hard concept. In reality, it just is a streamlined version of what we already have been doing for years. So take the knowledge here, and expand your horizons on what code quality and reusability mean.