Monorepo Tools Guide: Turborepo, Nx, Bazel, and Moon
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.
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
- JavaScript/TypeScript monorepos
- Small to medium size (5-50 packages)
- Teams that value simplicity over features
- You want fast setup with minimal configuration
When It Does Not
- Polyglot repos (Go, Python, Java alongside JS)
- Need for code generation or scaffolding
- Complex task graphs beyond simple dependency chains
- Need fine-grained control over what constitutes a "change"
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
- Large JavaScript/TypeScript monorepos (50+ packages)
- Teams that want scaffolding and code generation
- You need module boundary enforcement as the team scales
- You want rich tooling for React, Angular, Node, or Next.js
When It Does Not
- Small monorepos where the overhead is not justified
- Polyglot repos (Nx has experimental Go/Rust support, but it is not mature)
- Teams that find the configuration and plugin system too complex
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
- Hundreds or thousands of packages across multiple languages
- You need hermetic, reproducible builds
- Your CI time is measured in hours and you need remote execution
- Google, Meta, Stripe-scale engineering organizations
When It Does Not
- Small to medium monorepos (the complexity is not justified)
- JavaScript-only projects (Turborepo or Nx are much simpler)
- Teams without dedicated build infrastructure engineers
- Rapid prototyping (Bazel slows you down initially)
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
- Polyglot monorepos (JS + Rust + Go + Python)
- You want Bazel-like correctness without Bazel-like complexity
- Consistent toolchain management across the team
- Medium to large repositories
- You value Rust-grade performance
When It Does Not
- You need a huge plugin ecosystem (Nx has more)
- Very small monorepos (Turborepo is simpler)
- Your team is not willing to adopt YAML-based task configuration
- You need extensive code generation features
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.