Building a Better Monorepo with TypeScript, Turborepo, or Nx

Unified Codebase and Simplified Development with different monorepo tools
Nov 6 2024 · 5 min read

Monorepo is a software development strategy that creates shared libraries and dependencies in a single repository. By centralizing codebases, improving collaboration, and streamlining dependency management, Monorepos offer a solution for crucial dependency management. 

This article explains the process of creating monorepos using well-known tools and technologies like TypeScript, Turborepo, and Nx, allowing you to choose the best tool based on your requirements.


If you’re a hands-on learner, I created a video to walk through this process. Feel free to follow along as you read!


Why Monorepo?

Imagine managing three separate applications for a project: a Web App, a Mobile App, and an Admin Panel. Each resides in its git repository and shares a common UI component library. Now think, when you need to update a critical button component for accessibility.

The process becomes a headache:

  • Update the component library and publish to NPM
  • Update each application one by one
  • Deal with version conflicts and dependencies
  • Ensure consistency across all projects

Sounds frustrating, right? This is where monorepos comes in—a simpler approach where all your projects live under one roof, making it easier to coordinate changes and maintain consistency across your codebase.

Let’s start with monorepos…


In the following sections, we will create basic utils monorepo which can be used anywhere in the project. 

 

Monorepo with Typescript

In this section, we will build a monorepo manually using just TypeScript and npm.

Initialize the Project

  • Create a monorepo example directory and set the root project using the below commands,
npm init -y
npm install typescript
npm install --save-dev @types/node ts-node

Organize Packages

Create a packages folder and a utils directory within it to act as a shared library.

mkdir -p packages/utils

Init npm inside utils with scope @monorepo,

npm init --scope @monorepo --workspace ./packages/utils -y

packages/utils/package.json will look like this,

{
  "name": "@monorepo/utils",
  "version": "1.0.0",
  "main": "index.js",
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

This will also add a workspace field in the root’s package.json like below. If you have multiple monorepos, you can update workspace using wildcards like "packages/*".

"workspaces": [
    "packages/utils" or "packages/*"
]

Configure TypeScript

  • We need to configure typescript in both the directory root and packages
npx tsc --init && cd packages/utils && npx tsc --init
  • You can update baseDir and rootDir in packages/utils’s tsconfig.jsonas needed. In my case it is,
"rootDir": "./src",
"outDir": "./dist", // make sure to update main

Note: Make sure to update the main field in the package.json based on your outDir. For me, it is dist/index.js .

  • As we need monorepo to be referenced in other projects, we need to add composite: true in utils/tsconfig.json .
  • Add utils reference in the project root’s tscofig.json
"compilerOptions": {...},
"references": [
     {"path": "./packages/utils" },
 ]
  • For module resolution and path mapping, we will add the following in the root’s tsconfig.json
"compilerOptions": {
  ...,
  "baseUrl": "./",              
  "paths": {
      "@monorepo/utils": ["packages/utils/*"],
   }
},

Add and Build Code

  • Write a sample function in packages/utils/src/index.ts
// packages/utils/src/index.ts

export function multiply(a: number, b: number): number {
    return a * b;
}
  • Add "build": "tsc --build" in the root and utils package.json scripts, then build with npm run build.

Usage

  • Import @monorepo/utils into the root file to verify the function works as expected.
npm install @monorepo/utils
  • You can use the monorepo package like below, 
import { multiply } from "@monorepo/utils";

const result = multiply(2, 3);
console.log(`result: ${result}`);

Yeah, You have created a basic monorepo setup. You can build as many as monorepos and use them like this. 

When to Use?

Monorepo’s with only typescript is useful for small, simple projects or experimental purposes without requiring third-party tooling.

Next, let’s create monorepo with Turborepo.

Note: Repeat till step-3 to init setup in both turbo and nx, then follow below steps.


Monorepo with Turborepo

Turborepo is a high-performance monorepo build tool that simplifies dependency management and speeds up builds through caching.

Follow the below steps to configure monorepo using Turborepo.

Set Up Turborepo

  • Install Turborepo as a dev dependency,
npm install turbo -D
  • Add packageManager to each package.json to ensure compatibility with Turborepo.
"packageManager": "npm@<npm-version>",

Configure Turborepo

Create a turbo.json in the root directory:

{
    "$schema": "https://turborepo.org/schema.json",
    "tasks": {
      "build": {
        "dependsOn": ["^build"],
        "outputs": ["dist/**"]
      },
      "dev": {
        "cache": false
      }
    }
  }
  • The tasks section defines tasks like build dependencies and caching behavior.

Add and Build Code

  • Add a data-transform package to illustrate Turborepo's dependency management.
  • Add code for transforming data in data-transform,
export function transformData(data: any): any {   
   return data.map((item) => ({ ...item, transformed: true })); 
}
  • Build the entire project using,
npx turbo build
  • Turborepo automatically caches results, speeding up subsequent builds.

Usage

You can install @monorepo/data-transform into the root or utils package, and verify it's working as expected.

When to Use?

Ideal for mid-size projects that need optimized build times and clear dependency management.

Now let’s create monorepo with NX.


Monorepo with NX

Nx offers a highly integrated monorepo experience, by implementing an efficient caching mechanism, storing results from previous executions.

It includes code generation tools that scaffold various applications, libraries, and components like React, Angular, Vue, etc…

Nx provides Nx cloud for distributed caching, making builds even faster in CI/CD and visual dependency graphs for a better understanding of project dependencies for complex projects.

Follow the below steps to configure monorepo using Nx.

Initialize Nx

  • Run Nx’s setup command
npx nx@latest init
  • Nx will generate an nx.json file and scaffold dependencies for managing projects. Add the below script in nx.json,
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "defaultBase": "master",
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/dist"], /** output directory **/
      "cache": true
    }
  }
}

Build using Nx

  • Nx has been configured to run your build tasks. You can run a single task like this,
npx nx build @monorepo/utils
  • Or multiple projects with a certain task(i.e build) like this,
npx nx run-many -t build

When to Use?

Nx is suited for large projects with complex dependency chains, making it ideal for enterprise-level applications.


Conclusion

Monorepo is a powerful way to unify your codebase, improve consistency, and reduce overhead, especially in teams handling multiple interconnected projects. Adopting a monorepo approach can lead to a cleaner development workflow, easier dependency management, and more scalable infrastructure for your applications.

Choosing the right tool from all three depends on factors like your project size, the frequency of cross-project dependencies, and the level of automation required: 

  1. TypeScript-only monorepo: This is a minimalistic approach, perfect for smaller projects. It’s straightforward but requires more manual configuration to handle dependencies and task orchestration.
  2. Turborepo: Provides simplicity and speed. It’s optimized for fast builds, especially in larger projects, by efficiently caching and optimizing task runs but it might be less customizable than Nx for highly complex setups.
  3. Nx: Ideal for complex, enterprise-level projects that demand advanced dependency management, and deep customization. Nx’s rich plugin ecosystem and project graph visualization can make managing large-scale monorepos far more efficient.

Similar articles


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.

canopas-logo
We build products that customers can't help but love!
Get in touch

Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.