Software Engineering Musings

The Promised-land of the TypeScript Monorepo (without Lerna or Nx)

July 27, 2020

I love working with monorepos, but I found it difficult to get one up and running. All resources I read were heavily biased towards using Lerna, and more recently Nx.

Now these are both great tools but if you aren’t building something to release publicly then I don’t believe they are the right choice.

Use Cases

The first time I realised that a monorepo may be the best tool for the job was when creating a React + TypeScript frontend, and Nest.js + TypeScript backend.

I had all my models defined in the backend, but needed them on the frontend too. Rather than duplicate code I decided to roll a monorepo that had a shared package they could both have as dependencies, put the relecant models in there, and the rest is history.

Another, more recent, example was a project with two React apps, both using the same UI library, and two Serverless stacks.

Using a monorepo, I was able to create a package that had React components with defined behaviours and styles, that could be used in both the apps. As well as a package for data models, for use between all the React and Serverless apps.

Additionally, I added yet another package to house some of the utility types I created to make the Lambdas in the Serverless projects easier to work with.

Before we go further

What lies beneath is a simple guide to working with a monorepo using Yarn Workspaces, there is no Lerna or Nx.

If you want to dive straight in, here’s the monorepo-mvp repository that we’ll be discussing. Though if you’re after something a bit more complete then check out this ts-monorepo, it’s brilliant.

Getting Setup

It’s really straightforward. In the root of your project you’ll need a package.json, in which we set private to true and indicate to yarn where your packages are;

{ "private": true, "workspaces": ["packages/*"] }

Keeping TypeScript Happy

From here you’ll want to create tsconfig.json and tsconfig.build.json. The latter will effectively serve as the base for your repository and look something like this;

// tsconfig.build.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "sourceMap": true
  },

  "exclude": ["node_modules", "dist"]
}

The regular tsconfig.json extends this, however we set a path and some more defaults.

// tsconfig.json
{
  "extends": "./tsconfig.build.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@mvp/*": ["packages/*/src"]
    },
    "jsx": "react",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Note: you should feel free, even encouraged, to make changes to this for your project’s requirements.

Adding a Package

When adding packages, you can just add them into packages/. The key, however, is to add a tsconfig.json that extends the base one we created earlier;

// packages/api/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "baseUrl": "./src"
  },
  "include": ["src"]
}

Using a Package

Just add your package to the dependencies of another, e.g.;

// packages/api/package.json
{
  "name": "@mvp/api",
  "dependencies": {
    "@mvp/common": "*"
  }
}

Then run a yarn install, and that’s about it.

Reasons to Use a Monorepo

Though the following will include some points from the use cases I covered above, reasons include, but are in no way limited to;

  • Easier to share data models between an API and client
  • You can create a common component kit separate to your app
  • Validation logic can be shared between front and back end packages
  • Any utility functions and types can be shared as you please
  • You can comfortably use a single testing framework
  • You don’t need to copy and paste code
  • No need to context switch between multiple git repositories
  • No need to use the less understood parts of git such as submodules
  • Update your code in one place and it’s immediately propogated to where it’s installed as a dependency
  • Share a configuration for other tools such as ESLint and Prettier
  • Lower net size of your node_modules

Something else I really love is the ability to run a command in all of your packages in one hit, the perfect example of this is running tests. By which you can run yarn workspaces run test and it will run the test script in all packages. How neat is that?

Caveats

I haven’t really found any yet, though I’ve noticed when I add a new, external package to one in the monorepo it can take some time for VS Code’s intellisense to work as expected. Honestly it’s a lot easier, and faster, to just close and reopen it. Outside of that it works perfectly.

In Summary

A monorepo is a great solution when the problem is sharing code between various items you’re building, and I wish I had of started making use of them sooner.