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    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Frequently Asked Questions

Find answers to common questions

A monorepo (monolithic repository) stores multiple projects in a single Git repository. Benefits include atomic commits across projects, shared tooling and configurations, easier code sharing and refactoring, unified versioning, and simplified dependency management. Companies like Google, Meta, and Microsoft use monorepos for these advantages.

Nx is a full-featured monorepo toolkit with code generators, dependency graph analysis, and deep framework integration (React, Angular, Node). Turborepo focuses specifically on build caching and task orchestration—it's simpler and faster for builds but has fewer features. Choose Nx for large teams needing scaffolding; Turborepo for simpler caching needs.

Monorepo tools hash inputs (source files, dependencies, env vars) for each task. If the hash matches a previous run, the cached output is restored instead of rebuilding. Remote caching shares this cache across CI runs and team members. A task that took 5 minutes might restore from cache in seconds.

Affected commands analyze Git changes to determine which projects changed and their dependents, then only run tasks on those projects. Instead of testing 50 packages, you might test 3 that actually changed. This dramatically speeds up CI/CD. Example: nx affected --target=test runs tests only for changed projects.

Sign up for Vercel (Turborepo's creator) and link your repo: npx turbo login then npx turbo link. For self-hosted, set TURBO_TOKEN, TURBO_TEAM, and TURBO_API environment variables. In turbo.json, caching is enabled by default. Remote cache is shared across all CI runs and developers automatically.

Use monorepo when projects are tightly coupled and change together, you want atomic commits across projects, team members work across multiple packages, and you value unified tooling. Use polyrepo when projects have different lifecycles and owners, you need independent versioning and deployment, or teams are fully autonomous with minimal shared code.

Use Changesets for semantic versioning: npx changeset creates versioning intents, npx changeset version updates package.json files, npx changeset publish publishes to npm. Each PR describes its changes; the tool aggregates them into changelogs and version bumps. Nx and Lerna also have built-in publishing workflows.

Task pipelines define dependencies between tasks. Example: 'build' depends on 'codegen', which depends on 'install'. The tool runs tasks in correct order, parallelizing independent work. Without pipelines, you'd run tasks sequentially or fail due to missing outputs. Define pipelines in nx.json or turbo.json.

Common structure: /apps for deployable applications (web, api, mobile), /packages or /libs for shared libraries, /tools for internal build tools. Each project has its own package.json. Use workspace protocol (workspace:*) for internal dependencies. Configure build tools to understand this structure for optimal caching.

Enable remote caching to reuse builds across runs. Use affected commands to only build/test what changed. Parallelize tasks across multiple CI runners. Split into multiple CI jobs (lint, test, build) that run concurrently. Use distributed task execution to spread work across machines. Cache dependencies separately from build outputs.

Engineering Excellence for Your Business

Our engineers build systems that scale. Clean architecture, comprehensive testing, and security-first development.