← All articles
ARCHITECTURE Monorepo Tools Guide: Turborepo, Nx, Bazel, and Moon 2026-02-15 · 9 min read · monorepo · turborepo · nx

Monorepo Tools Guide: Turborepo, Nx, Bazel, and Moon

Architecture 2026-02-15 · 9 min read monorepo turborepo nx bazel moon build-tools architecture

Monorepo Tools Guide: Turborepo, Nx, Bazel, and Moon

A monorepo without proper tooling is worse than separate repos. You get all the drawbacks -- slow builds, tangled dependencies, CI that runs everything on every change -- without the benefits. The right build orchestrator gives you fast incremental builds, intelligent task scheduling, and the ability to scale from a handful of packages to hundreds without your CI time growing linearly.

Comparison of monorepo build tools for developers

This guide covers the four most relevant monorepo tools in 2026: Turborepo for simplicity, Nx for the full-featured JavaScript ecosystem, Bazel for massive polyglot repositories, and Moon for the Rust-influenced newcomer that is gaining traction fast.

What Monorepo Tools Actually Do

Before comparing tools, it helps to understand the core problems they solve:

Task orchestration: You have 30 packages. Package A depends on B and C. B depends on D. When you change D, the tool needs to rebuild D, then B, then A -- in that order, and nothing else.

Caching: You built package B yesterday and nothing changed. The tool should skip the build entirely and serve the cached output. This works locally and remotely (so your coworker or CI runner also skips the build).

Affected detection: You changed one line in one package. The tool determines which packages are affected by that change and only runs tasks for those packages.

Parallel execution: Packages E, F, and G are independent. The tool runs their builds concurrently across all available CPU cores.

Quick Comparison

Feature Turborepo Nx Bazel Moon
Language Rust TypeScript Java + Starlark Rust
Primary ecosystem JavaScript/TypeScript JavaScript/TypeScript Any language Any language
Local caching Yes Yes Yes Yes
Remote caching Yes (Vercel) Yes (Nx Cloud) Yes (built-in) Yes (moonbase)
Task orchestration Yes Yes Yes Yes
Dependency graph Hash-based Advanced (project graph) Exact (action graph) Project graph
Code generation No Yes (extensive) No Yes (templates)
Affected detection Yes Yes (advanced) Yes (exact) Yes
Configuration turbo.json nx.json + project.json BUILD files moon.yml
Learning curve Low Medium Very high Low-medium
Language support JS/TS only Primarily JS/TS Any Any

Turborepo: Start Here

Turborepo is the simplest monorepo tool. It does task orchestration and caching, and it does them well. If your monorepo has fewer than 50 packages and you are in the JavaScript/TypeScript ecosystem, Turborepo is the right starting point.

Setup

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

# Or add Turborepo to an existing monorepo
cd my-existing-monorepo
bun add -D turbo

Configuration

Turborepo uses a single turbo.json at the root:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "inputs": ["src/**", "tsconfig.json", "package.json"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "tests/**"]
    },
    "lint": {
      "inputs": ["src/**", "*.config.*"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json"]
    }
  }
}

The ^build syntax means "run the build task in all dependencies first." This is how Turborepo understands task ordering.

Workspace Structure

my-monorepo/
├── turbo.json
├── package.json
├── apps/
│   ├── web/          # Next.js frontend
│   │   └── package.json
│   └── api/          # Express backend
│       └── package.json
└── packages/
    ├── ui/           # Shared React components
    │   └── package.json
    ├── config/       # Shared ESLint, TypeScript configs
    │   └── package.json
    └── utils/        # Shared utility functions
        └── package.json

Running Tasks

# Build everything (respects dependency order, uses cache)
turbo build

# Build only affected packages
turbo build --filter=...[HEAD~1]

# Build a specific package and its dependencies
turbo build --filter=web...

# Run dev servers for web and all its dependencies
turbo dev --filter=web...

# Run tests only in packages that changed
turbo test --filter=...[origin/main]

Caching in Action

$ turbo build
# First run: builds everything
 Tasks:    5 successful, 5 total
 Cached:   0 cached, 5 total
 Time:     34.2s

$ turbo build
# Second run: everything cached
 Tasks:    5 successful, 5 total
 Cached:   5 cached, 5 total
 Time:     0.3s

Remote Caching

# Login to Vercel for remote caching
turbo login

# Link your repo
turbo link

# Now cached artifacts are shared across machines
# Your CI runner uses the cache from your local build

When Turborepo Fits

When It Does Not

Nx: The Full-Featured Option

Nx is the most feature-rich monorepo tool for the JavaScript ecosystem. Beyond task orchestration and caching, it provides code generation, dependency visualization, module boundary enforcement, and a plugin ecosystem.

Setup

# Create a new Nx workspace
npx create-nx-workspace my-workspace

# Add Nx to an existing monorepo
npx nx@latest init

Configuration

Nx uses nx.json at the root and optional project.json files per package:

// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/**/*.spec.*"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  },
  "plugins": [
    "@nx/react",
    "@nx/node"
  ]
}
// packages/ui/project.json
{
  "name": "ui",
  "targets": {
    "build": {
      "executor": "@nx/vite:build",
      "outputs": ["{projectRoot}/dist"],
      "options": {
        "outputPath": "dist/packages/ui"
      }
    },
    "test": {
      "executor": "@nx/vite:test"
    },
    "storybook": {
      "executor": "@nx/storybook:storybook",
      "options": {
        "port": 6006
      }
    }
  }
}

Code Generation

Nx generators scaffold new packages, components, and configurations:

# Generate a new React library
nx generate @nx/react:library shared-ui --directory=packages/shared-ui

# Generate a new Express application
nx generate @nx/node:application api --directory=apps/api

# Generate a component inside a library
nx generate @nx/react:component Button --project=shared-ui

# Custom generator (scaffold your own patterns)
nx generate @nx/workspace:library my-lib --directory=packages/my-lib

Module Boundary Enforcement

Nx can enforce dependency rules -- preventing, for example, a UI library from importing a backend package:

// .eslintrc.json (root)
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "scope:frontend",
            "onlyDependOnLibsWithTags": ["scope:frontend", "scope:shared"]
          },
          {
            "sourceTag": "scope:backend",
            "onlyDependOnLibsWithTags": ["scope:backend", "scope:shared"]
          }
        ]
      }
    ]
  }
}

Dependency Graph Visualization

# Open an interactive dependency graph in your browser
nx graph

# Show what's affected by your current changes
nx affected:graph

When Nx Fits

When It Does Not

Bazel: The Enterprise Heavyweight

Bazel (originally from Google) is designed for monorepos with thousands of packages across multiple languages. It provides hermetic builds (every build is reproducible), exact dependency tracking, and remote execution. The trade-off is complexity.

Configuration

Bazel uses BUILD files (written in Starlark, a Python dialect) to define targets:

# packages/api/BUILD.bazel
load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test")

go_library(
    name = "api_lib",
    srcs = glob(["*.go"]),
    importpath = "github.com/myorg/monorepo/packages/api",
    deps = [
        "//packages/auth:auth_lib",
        "//packages/database:db_lib",
        "@com_github_gin_gonic_gin//:gin",
    ],
    visibility = ["//visibility:private"],
)

go_binary(
    name = "api",
    embed = [":api_lib"],
    visibility = ["//visibility:public"],
)

go_test(
    name = "api_test",
    srcs = glob(["*_test.go"]),
    embed = [":api_lib"],
    deps = [
        "@com_github_stretchr_testify//assert",
    ],
)
# packages/web/BUILD.bazel
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@aspect_rules_js//js:defs.bzl", "js_binary")

ts_project(
    name = "web_lib",
    srcs = glob(["src/**/*.ts", "src/**/*.tsx"]),
    tsconfig = ":tsconfig.json",
    deps = [
        "//packages/ui:ui_lib",
        "//:node_modules/react",
        "//:node_modules/react-dom",
    ],
)

Running Bazel

# Build a specific target
bazel build //packages/api:api

# Run tests
bazel test //packages/api:api_test

# Build everything
bazel build //...

# Query the dependency graph
bazel query "deps(//packages/api:api)" --output graph

# Find what depends on a package
bazel query "rdeps(//..., //packages/auth:auth_lib)"

Remote Execution and Caching

Bazel's remote execution distributes builds across a cluster:

# .bazelrc
build --remote_cache=grpc://cache.mycompany.com:9092
build --remote_executor=grpc://executor.mycompany.com:9092
build --remote_instance_name=my-project

When Bazel Fits

When It Does Not

Moon: The Modern Contender

Moon is a Rust-based monorepo tool that supports multiple languages (JavaScript, TypeScript, Rust, Go, Python, and more). It takes design cues from Bazel's correctness guarantees but aims for Turborepo's simplicity.

Setup

# Install Moon
curl -fsSL https://moonrepo.dev/install/moon.sh | bash

# Initialize in your repo
moon init

Configuration

Moon uses YAML configuration:

# .moon/workspace.yml
projects:
  - "apps/*"
  - "packages/*"

vcs:
  manager: git
  defaultBranch: main

hasher:
  walkStrategy: glob
# .moon/toolchain.yml
node:
  version: "20.11.0"
  packageManager: bun
  bun:
    version: "1.1.0"

typescript:
  projectConfigFileName: tsconfig.json
  routeOutDirToCache: true
# apps/web/moon.yml
type: application
language: typescript

tasks:
  build:
    command: bun run build
    inputs:
      - "src/**/*"
      - "tsconfig.json"
    outputs:
      - "dist"
    deps:
      - "~:typecheck"
      - "^:build"

  dev:
    command: bun run dev
    local: true

  test:
    command: bun test
    inputs:
      - "src/**/*"
      - "tests/**/*"

  typecheck:
    command: tsc --noEmit
    inputs:
      - "src/**/*"
      - "tsconfig.json"

Running Tasks

# Run a task for a specific project
moon run web:build

# Run a task across all projects
moon run :build

# Run affected tasks (only what changed)
moon ci

# Check the dependency graph
moon project-graph

# Run with verbose output
moon run web:build --log trace

Toolchain Management

Moon manages tool versions for you -- no need for nvm, fnm, or manual Node.js installation:

# .moon/toolchain.yml
node:
  version: "20.11.0"  # Moon installs this automatically
  packageManager: bun
  bun:
    version: "1.1.0"  # And this

Every developer and CI runner uses the exact same versions. No "works on my machine" issues.

When Moon Fits

When It Does Not

Migration Strategies

From No Tooling to Turborepo

# 1. Install Turborepo
bun add -D turbo

# 2. Add turbo.json with your tasks
# 3. Add scripts to root package.json
{
  "scripts": {
    "build": "turbo build",
    "test": "turbo test",
    "lint": "turbo lint",
    "dev": "turbo dev"
  }
}

From Turborepo to Nx

# Nx provides an automated migration
npx nx@latest init

# This adds Nx alongside your existing setup
# You can migrate incrementally

From Lerna to Turborepo or Nx

# Lerna 7+ uses Nx under the hood
# Update Lerna and you get Nx caching for free
npx lerna@latest init

# Or migrate fully to Turborepo
# 1. Remove Lerna
# 2. Add Turborepo
# 3. Map lerna.json tasks to turbo.json

CI Integration

All four tools integrate with CI. The key optimization is remote caching -- so CI does not rebuild what a developer already built locally.

Turborepo + GitHub Actions

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install
      - run: turbo build test lint
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Nx + GitHub Actions

name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Nx needs full history for affected detection
      - uses: oven-sh/setup-bun@v2
      - run: bun install
      - uses: nrwl/nx-set-shas@v4  # Determines base/head for affected
      - run: npx nx affected -t build test lint

Decision Framework

If you need... Choose
Simplest possible setup for JS/TS Turborepo
Code generation, plugins, module boundaries Nx
Polyglot support with managed toolchains Moon
Thousands of packages, hermetic builds Bazel
Starting from scratch, unsure of scale Turborepo (migrate later if needed)
Migrating from Lerna Nx (Lerna 7 uses Nx internally)

Summary

The monorepo tool you choose depends on your scale, your language mix, and how much complexity you are willing to manage. Turborepo is the right default for JavaScript/TypeScript teams -- it is simple, fast, and gets out of your way. Nx is the upgrade path when you need code generation, module boundaries, and a richer plugin ecosystem. Moon is the choice for polyglot repos that want modern Rust-grade tooling without Bazel's learning curve. Bazel is for organizations where build correctness and remote execution at massive scale justify the significant investment in configuration and infrastructure. Start simple. Migrate when you outgrow your current tool. The switching cost between Turborepo and Nx is low, and you will know when you need the upgrade.