Home/Blog/Monorepo Management: Nx, Turborepo, and Build Optimization
Software Engineering

Monorepo Management: Nx, Turborepo, and Build Optimization

Learn how to manage monorepos effectively with Nx, Turborepo, and Bazel. Covers build caching, dependency management, affected commands, and CI/CD optimization.

By Inventive HQ Team
Monorepo Management: Nx, Turborepo, and Build Optimization

As codebases grow and teams scale, managing multiple related projects becomes increasingly complex. Monorepos—storing multiple projects in a single repository—offer solutions for code sharing, atomic changes, and unified tooling. This guide covers when to use monorepos, which tools to choose, and how to optimize build performance at scale.

Monorepo vs Polyrepo

┌─────────────────────────────────────────────────────────────┐
│                    POLYREPO STRUCTURE                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│   │  repo-web   │  │  repo-api   │  │ repo-shared │         │
│   │  ├── src/   │  │  ├── src/   │  │  ├── src/   │         │
│   │  ├── tests/ │  │  ├── tests/ │  │  ├── tests/ │         │
│   │  └── ...    │  │  └── ...    │  │  └── ...    │         │
│   └─────────────┘  └─────────────┘  └─────────────┘         │
│         │                │                │                  │
│         └────────────────┼────────────────┘                  │
│                          │                                   │
│              Published to npm registry                       │
│                                                              │
├─────────────────────────────────────────────────────────────┤
│                    MONOREPO STRUCTURE                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────────────────────────────────┐               │
│   │              single-repo                 │               │
│   │  ├── apps/                              │               │
│   │  │   ├── web/                           │               │
│   │  │   └── api/                           │               │
│   │  ├── packages/                          │               │
│   │  │   ├── shared-ui/                     │               │
│   │  │   ├── shared-utils/                  │               │
│   │  │   └── config/                        │               │
│   │  └── package.json (workspaces)          │               │
│   └─────────────────────────────────────────┘               │
│                                                              │
│         Internal imports via workspace protocol             │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Decision Matrix

FactorMonorepo FavoredPolyrepo Favored
Code sharingFrequent sharing across projectsMinimal shared code
Team structureCross-functional teamsAutonomous teams
Release cadenceCoordinated releasesIndependent releases
DependenciesShared dependency versionsDifferent dep versions
ToolingUnified build/test/lintProject-specific tooling
Repo sizeManageable (<10GB)Very large binaries
Open sourceInternal/enterprisePublic packages

Monorepo Tools Comparison

┌─────────────────────────────────────────────────────────────┐
│                  MONOREPO TOOL LANDSCAPE                     │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   Full-Featured Frameworks                                   │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  Nx          │  Complete monorepo solution           │   │
│   │              │  Generators, graph, plugins, cloud    │   │
│   ├─────────────────────────────────────────────────────┤   │
│   │  Bazel       │  Google's build system                │   │
│   │              │  Massive scale, complex setup         │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
│   Build Focused                                              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  Turborepo   │  Fast builds, simple config           │   │
│   │              │  Remote caching, task pipelines       │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
│   Package Management                                         │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  Lerna       │  npm publishing workflows              │   │
│   │  Rush        │  Microsoft's enterprise solution      │   │
│   │  pnpm        │  Workspace-native package manager     │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Tool Selection Guide

ToolBest ForLearning CurveRemote Cache
NxLarge teams, framework-heavy projectsMediumNx Cloud (free tier)
TurborepoSimple setups, Vercel usersLowVercel (free tier)
BazelMassive scale (1000+ engineers)HighSelf-hosted
Lernanpm package publishingLowVia Nx
pnpmWorkspace management onlyLowNone built-in

Setting Up Turborepo

Turborepo focuses on build optimization with minimal configuration:

# Create new monorepo
npx create-turbo@latest my-monorepo

# Or add to existing repo
npm install turbo --save-dev

Project structure:

my-monorepo/
├── apps/
│   ├── web/
│   │   └── package.json
│   └── api/
│       └── package.json
├── packages/
│   ├── ui/
│   │   └── package.json
│   └── config/
│       └── package.json
├── package.json
└── turbo.json

Root package.json:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test"
  }
}

turbo.json:

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Turborepo Remote Caching

# Login to Vercel
npx turbo login

# Link repository
npx turbo link

# Now builds are cached remotely
npx turbo build
# First run: builds everything
# Second run: restores from cache in seconds

Setting Up Nx

Nx provides a complete monorepo solution with code generation and plugins:

# Create new Nx workspace
npx create-nx-workspace@latest my-workspace

# Or add Nx to existing repo
npx nx@latest init

Nx workspace structure:

my-workspace/
├── apps/
│   ├── web/
│   │   ├── src/
│   │   └── project.json
│   └── api/
│       ├── src/
│       └── project.json
├── libs/
│   ├── shared-ui/
│   │   └── project.json
│   └── shared-utils/
│       └── project.json
├── nx.json
└── package.json

nx.json:

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "lint": {
      "cache": true
    },
    "test": {
      "cache": true
    }
  },
  "defaultBase": "main"
}

project.json example:

{
  "name": "web",
  "sourceRoot": "apps/web/src",
  "projectType": "application",
  "targets": {
    "build": {
      "executor": "@nx/next:build",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/web"
      }
    },
    "serve": {
      "executor": "@nx/next:server",
      "options": {
        "buildTarget": "web:build"
      }
    },
    "test": {
      "executor": "@nx/jest:jest",
      "options": {
        "jestConfig": "apps/web/jest.config.ts"
      }
    }
  }
}

Nx Commands

# Run task for specific project
nx build web
nx test api

# Run affected commands (only changed projects)
nx affected -t build
nx affected -t test

# View dependency graph
nx graph

# Generate new projects
nx g @nx/react:app my-new-app
nx g @nx/react:lib my-new-lib

# Enable remote caching
nx connect

Task Pipelines and Caching

Understanding Task Dependencies

┌─────────────────────────────────────────────────────────────┐
│                    TASK PIPELINE                             │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   "dependsOn": ["^build"]                                   │
│                                                              │
│   ^  = dependencies of this package                         │
│                                                              │
│   Example: Building 'web' app                               │
│                                                              │
│   ┌─────────────────┐                                       │
│   │   shared-ui     │  ── build first                       │
│   │   (dependency)  │                                       │
│   └────────┬────────┘                                       │
│            │                                                 │
│            ▼                                                 │
│   ┌─────────────────┐                                       │
│   │  shared-utils   │  ── build second                      │
│   │   (dependency)  │                                       │
│   └────────┬────────┘                                       │
│            │                                                 │
│            ▼                                                 │
│   ┌─────────────────┐                                       │
│   │      web        │  ── build last                        │
│   │  (application)  │                                       │
│   └─────────────────┘                                       │
│                                                              │
│   Parallel execution where possible                          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Cache Configuration

Turborepo cache keys:

{
  "tasks": {
    "build": {
      "outputs": ["dist/**"],
      "inputs": [
        "src/**",
        "!src/**/*.test.ts"
      ]
    }
  },
  "globalEnv": ["NODE_ENV", "CI"],
  "globalDependencies": [".env"]
}

Nx cache configuration:

{
  "targetDefaults": {
    "build": {
      "inputs": [
        "default",
        "^default",
        "{projectRoot}/**/*.ts",
        "!{projectRoot}/**/*.spec.ts"
      ],
      "outputs": ["{projectRoot}/dist"]
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/jest.config.ts"
    ]
  }
}

Affected Commands

Run tasks only on changed projects and their dependents:

# Turborepo
npx turbo build --filter=...[origin/main]

# Nx
nx affected -t build
nx affected -t test
nx affected -t lint

# Specify comparison base
nx affected -t build --base=main --head=HEAD

How Affected Analysis Works

┌─────────────────────────────────────────────────────────────┐
│                  AFFECTED ANALYSIS                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   Changed file: packages/shared-utils/src/format.ts         │
│                                                              │
│   Dependency Graph:                                          │
│                                                              │
│   ┌────────────┐    ┌────────────┐    ┌────────────┐        │
│   │    web     │───▶│ shared-ui  │───▶│shared-utils│ ◀──    │
│   └────────────┘    └────────────┘    └────────────┘  │     │
│         │                                              │     │
│         │           ┌────────────┐                    │     │
│         └──────────▶│  config    │                    │     │
│                     └────────────┘                    │     │
│                                                       │     │
│   ┌────────────┐    ┌────────────┐                   │     │
│   │    api     │───▶│shared-utils│ ◀─────────────────┘     │
│   └────────────┘    └──────┬─────┘                         │
│                            │ CHANGED                        │
│                                                              │
│   Affected: shared-utils, shared-ui, web, api               │
│   Not affected: config (no dependency on shared-utils)       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

CI/CD for Monorepos

GitHub Actions with Turborepo

name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Needed for affected detection

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install

      - name: Build
        run: pnpm turbo build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Test
        run: pnpm turbo test

GitHub Actions with Nx

name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for affected detection

      - uses: nrwl/nx-set-shas@v4
        with:
          main-branch-name: main

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install

      - name: Run affected commands
        run: |
          npx nx affected -t lint --parallel=3
          npx nx affected -t test --parallel=3 --configuration=ci
          npx nx affected -t build --parallel=3

Distributed Task Execution

For large monorepos, distribute work across multiple CI agents:

Nx Cloud distributed execution:

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm install

      - name: Start Nx Agents
        run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"

      - name: Run Tasks
        run: npx nx affected -t lint test build --parallel=3

Versioning and Publishing

Changesets for Version Management

# Install changesets
npm install @changesets/cli --save-dev
npx changeset init

# When making changes, add a changeset
npx changeset
# Interactive: select packages, bump type, description

# In CI: version packages
npx changeset version

# In CI: publish to npm
npx changeset publish

Changeset file (.changeset/happy-dogs-jump.md):

---
"@myorg/shared-utils": minor
"@myorg/web": patch
---

Added new date formatting utilities to shared-utils.
Updated web app to use new formatters.

Lerna Publishing

# Initialize Lerna
npx lerna init

# Publish changed packages
npx lerna publish

# Publish with specific version bump
npx lerna publish patch

# Publish from CI
npx lerna publish from-package --yes

Performance Optimization

Optimizing Build Performance

TechniqueTool SupportImpact
Local cachingAll50-80% faster repeat builds
Remote cachingTurbo, Nx, Bazel90%+ faster CI
Affected commandsTurbo, Nx, BazelSkip unchanged projects
Parallel executionAllUtilize all CPU cores
Distributed executionNx Cloud, BazelScale across machines
Incremental buildsAllOnly rebuild changed files

Debugging Cache Misses

# Turborepo: see why cache missed
TURBO_LOG_VERBOSITY=1 npx turbo build

# Nx: show cache hash details
NX_VERBOSE_LOGGING=true npx nx build web

# Common causes:
# - Environment variable differences
# - Different dependency versions
# - Changed global files (package-lock.json)
# - Timestamps in outputs

Monorepo Best Practices

Directory Structure

monorepo/
├── apps/                    # Deployable applications
│   ├── web/
│   ├── mobile/
│   └── api/
├── packages/                # Shared libraries
│   ├── ui/                  # Component library
│   ├── utils/               # Utility functions
│   ├── config/              # Shared configs
│   └── types/               # Shared TypeScript types
├── tools/                   # Build tools, scripts
├── .github/                 # CI/CD workflows
├── package.json             # Root package, workspaces
├── turbo.json               # Turborepo config
└── nx.json                  # Nx config (if using Nx)

Internal Dependencies

Use workspace protocol for internal packages:

{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:^1.0.0"
  }
}
ProtocolBehavior
workspace:*Always use local version
workspace:^1.0.0Use local if compatible
workspace:~1.0.0Use local if compatible (stricter)

Code Sharing Guidelines

┌─────────────────────────────────────────────────────────────┐
│              CODE SHARING DECISION TREE                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   Is this code used by multiple projects?                   │
│                    │                                        │
│         ┌─────────┴─────────┐                               │
│         │                   │                                │
│        NO                  YES                               │
│         │                   │                                │
│         ▼                   ▼                                │
│   Keep in app         Is it stable?                         │
│                             │                                │
│              ┌──────────────┴──────────────┐                │
│              │                             │                 │
│             NO                            YES                │
│              │                             │                 │
│              ▼                             ▼                 │
│   Keep in app until stable       Create shared package      │
│                                            │                 │
│                          ┌─────────────────┴───────┐        │
│                          │                         │        │
│                      Internal                 External      │
│                          │                         │        │
│                          ▼                         ▼        │
│                    packages/lib           Publish to npm    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Let's turn this knowledge into action

Get a free 30-minute consultation with our experts. We'll help you apply these insights to your specific situation.