Best Monorepo Tools in 2026: Turborepo vs Nx vs Moon
TL;DR
Turborepo for simple, fast build caching; Nx for enterprise monorepos with plugins; Moon for polyglot (multi-language) repos. Turborepo (~2M weekly downloads) was acquired by Vercel — fast setup, excellent caching, minimal config. Nx (~5M downloads) has the richest ecosystem with generators, affected commands, and first-class support for React, Angular, and NestJS. Moon (~50K downloads) is newer, Rust-based, and supports Node.js + Rust + Go in the same repo.
Key Takeaways
- Nx: ~5M weekly downloads — most features, best plugins, powers large enterprise codebases
- Turborepo: ~2M downloads — simplest setup, Vercel-backed, best for JS/TS monorepos
- Moon: ~50K downloads — Rust-based, polyglot support, built-in toolchain management
- Remote caching — Turborepo uses Vercel Remote Cache; Nx uses Nx Cloud; Moon uses moonrepo.dev
- All three — support task dependencies, incremental builds, and affected-only runs
Why Monorepos and Why These Tools Matter
A monorepo (single repository with multiple packages) provides concrete engineering benefits: refactoring across packages is atomic, shared code is always up-to-date across consumers, and CI configuration covers all packages in one place. The cost is build times — without tooling, a monorepo with 20 packages builds all 20 packages on every change, even when only one changed.
Monorepo build tools solve this with two key mechanisms: task dependency graphs (build api after building database, build database after building types) and incremental caching (if database hasn't changed since the last build, use the cached output). These two mechanisms together mean that CI builds in a well-configured monorepo are roughly proportional to what changed, not to repository size.
The second problem these tools solve is developer experience: running pnpm dev in a monorepo should start all services simultaneously with correct startup ordering. Turborepo's persistent: true task type handles this. Without tooling, running multi-service development requires multiple terminal windows with manual ordering.
Turborepo (Simple, Fast)
// turbo.json — minimal config
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"], // Run after dependencies' build
"outputs": ["dist/**", ".next/**"],
"cache": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"outputs": [],
"cache": true
},
"dev": {
"cache": false,
"persistent": true // Long-running task
}
}
}
// package.json — pnpm workspace monorepo
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"dev": "turbo dev"
},
"devDependencies": {
"turbo": "latest"
}
}
# Turborepo commands
turbo build # Build all packages
turbo build --filter=./apps/web # Build only web app + its deps
turbo build --filter=...my-lib # Build packages that depend on my-lib
turbo build --affected # Build only packages changed vs main
turbo build --dry-run # Preview what would run
turbo build --force # Ignore cache
turbo build --remote-only # Use Vercel Remote Cache
Turborepo's key design principle is minimalism: the turbo.json configuration defines task dependencies and outputs in a simple JSON format, and that's most of what you need. There are no generators, no project scaffolding, no plugin system. It does one thing — orchestrate tasks with caching — and does it well.
The dependsOn: ["^build"] pattern is Turborepo's core abstraction. The ^ prefix means "run this task in all dependencies first." This ensures the task graph respects your workspace dependency structure automatically: if apps/web depends on packages/ui, then turbo build --filter=apps/web runs packages/ui's build task first, then apps/web's.
Remote caching via Vercel is free for Vercel users. The cache stores build artifacts (identified by a hash of inputs) in Vercel's CDN. When CI runs turbo build and encounters a cache hit, it downloads the artifact instead of running the build. For large builds, this can reduce CI time from 10 minutes to 30 seconds on unchanged packages.
The Vercel acquisition brings both benefits and concerns. The benefits: active development, integration with Vercel deployment, and good funding. The concern: Turborepo's remote cache is tied to Vercel's infrastructure. Self-hosted remote caching is available via @turborepo/remote-cache with S3/R2/Minio backends, but it's less polished than the Vercel-hosted option.
Nx (Feature-Rich)
# Nx — create workspace
npx create-nx-workspace@latest my-workspace --preset=ts
# Add apps
npx nx g @nx/next:app web
npx nx g @nx/express:app api
npx nx g @nx/react:lib ui
// nx.json — task configuration
{
"defaultBase": "main",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"cache": true
}
},
"namedInputs": {
"production": [
"default",
"!{projectRoot}/**/*.spec.ts",
"!{projectRoot}/jest.config.ts"
]
}
}
# Nx commands — affected builds
nx build web # Build web
nx run-many --target=build # Build all projects
nx affected --target=build # Build only affected by changes
nx affected --target=test --base=main --head=HEAD # Test only changed
nx graph # Visualize project dependency graph
nx lint my-lib --fix # Lint with auto-fix
# Nx generators — scaffold with best practices
nx g @nx/next:page ProductPage --project=web
nx g @nx/react:component Button --project=ui --export
nx g @nx/express:resource users --project=api
# Nx plugins — first-class support
@nx/next # Next.js
@nx/react # React
@nx/angular # Angular
@nx/nestjs # NestJS
@nx/storybook # Storybook
@nx/docker # Docker
@nx/playwright # Playwright E2E
// Nx — project.json (per-project task config)
{
"name": "web",
"targets": {
"build": {
"executor": "@nx/next:build",
"options": {
"outputPath": "dist/apps/web"
},
"configurations": {
"production": {
"optimization": true
}
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/web/jest.config.ts"
}
}
}
}
Nx's differentiating features are its generators and its dependency graph visualization. Generators (invoked via nx g @nx/next:page) scaffold new files following the project's conventions — they're essentially typed templates that enforce consistency across a codebase. In a 20-person engineering team, generators ensure new pages, components, and services are created consistently without each developer reinventing the file structure.
The nx graph command generates an interactive dependency visualization showing which projects depend on which. For large monorepos, this graph is essential for understanding blast radius: changing packages/database might affect 12 downstream packages, and the graph makes this visible before you run CI.
Nx's affected commands are more sophisticated than Turborepo's equivalent. Nx tracks not just file changes but which TypeScript imports changed — if you modify an interface in packages/types, it can identify exactly which consuming packages need to rebuild based on actual import analysis. Turborepo uses simpler file-based hashing.
The tradeoff with Nx is configuration complexity. The project.json files, executor configuration, and plugin system add overhead compared to Turborepo's minimal approach. For teams comfortable with the setup cost, Nx's capabilities scale better to large, complex codebases.
Moon (Polyglot, Rust-Powered)
# .moon/workspace.yml — Moon configuration
vcs:
manager: git
defaultBranch: main
projects:
- apps/*
- packages/*
node:
version: 20.10.0
packageManager: pnpm
pnpmVersion: 9.0.0
# moon.yml — per-project task config
tasks:
build:
command: pnpm build
inputs:
- src/**/*
- package.json
outputs:
- dist
test:
command: pnpm test
inputs:
- src/**/*
- tests/**/*
# Moon commands
moon run :build # Run build across all projects
moon run web:build # Run build for web project only
moon run :test --affected # Test only affected projects
moon check web # Run all tasks for web
Moon's unique value proposition is polyglot support and toolchain management. Turborepo and Nx are JavaScript-centric — they work within Node.js's package ecosystem. Moon can orchestrate builds for Node.js, Rust, Go, and other language projects in the same repository, treating each language's build system as a black box with consistent task orchestration on top.
The toolchain management feature pins Node.js, npm, pnpm, and Yarn versions at the repository level and automatically installs the correct versions per environment. Teams that maintain multiple projects on different Node.js versions, or that want to enforce consistent tooling versions across team members without relying on .nvmrc files, benefit from Moon's approach.
Moon is written in Rust, making it faster than Node.js-based alternatives for pure task orchestration overhead. The practical performance difference is small — at the scale most teams operate, the time saved by Moon's Rust speed is under a minute per CI run.
Build Cache Performance
| Tool | Local Cache | Remote Cache | First Build | Cached Build |
|---|---|---|---|---|
| Turborepo | ✅ (fs) | Vercel (free tier) | Fast | <1s (hit) |
| Nx | ✅ (fs) | Nx Cloud (free tier) | Fast | <1s (hit) |
| Moon | ✅ (fs) | moonrepo.dev | Faster (Rust) | <1s (hit) |
| No tool | ❌ | ❌ | Slow | Slow |
Migrating to a Monorepo
If you're starting from multiple separate repositories ("polyrepo"), migration to a monorepo follows a predictable pattern:
- Create the monorepo structure with your chosen workspace tool (pnpm workspaces is the standard)
- Add your chosen orchestration tool (Turborepo, Nx, or Moon) with basic
buildandtesttask definitions - Move packages one at a time, validating that each package's tasks run correctly
- Extract shared code into
packages/— shared types, utilities, UI components, database schemas - Enable remote caching and measure CI time improvements
Teams that approach monorepo migration incrementally — adding new packages rather than moving existing ones — often find the migration slower but less risky. The initial value comes from sharing code between new projects; existing projects migrate when there's a clear benefit.
When to Choose
| Scenario | Pick |
|---|---|
| Small JS/TS monorepo, simple needs | Turborepo |
| Already use Vercel for deployment | Turborepo |
| Large org, need generators + scaffolding | Nx |
| Angular, React + NestJS enterprise app | Nx |
| Polyglot repo (JS + Rust + Go) | Moon |
| Need per-language toolchain management | Moon |
| Maximum ecosystem / plugin support | Nx |
| CI speed is the primary concern | Turborepo (simplest to tune) |
Testing Strategy in Monorepos
Monorepos amplify testing challenges in one direction while solving others. The shared codebase enables natural cross-package integration testing, but a failing test in one package can block CI for unrelated changes elsewhere.
Affected-Only Testing
The most impactful monorepo testing pattern is running only tests affected by the current change:
# Turborepo — affected testing
turbo test --filter=...[origin/main]
# Tests all packages that changed or depend on changed packages
# Nx — affected with TypeScript import analysis
nx affected --target=test
# Analyzes actual TypeScript imports, not just package.json dependencies
# Moon — affected testing
moon run :test --affected
Nx's import analysis is more accurate for complex dependency trees. If you change a type signature in packages/shared-types and 8 packages import from it, Nx identifies exactly which packages need re-testing based on actual import statements. Turborepo and Moon rely on workspace dependency declarations — packages that import from a shared package without listing it in package.json are missed.
Shared Test Configuration
Each package in a monorepo can have its own test runner configuration, or share a root-level config:
// vitest.config.base.ts — root level
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
coverage: {
provider: 'v8',
reporter: ['lcov'],
},
},
});
// packages/ui/vitest.config.ts — extends base
import { mergeConfig } from 'vitest/config';
import base from '../../vitest.config.base';
import react from '@vitejs/plugin-react';
export default mergeConfig(base, defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
},
}));
This pattern avoids configuration drift between packages. The packages/api package uses environment: 'node' while packages/ui uses environment: 'jsdom', but both share coverage and reporter settings.
Integration Testing Across Package Boundaries
Unit tests verify individual packages in isolation. The most valuable monorepo tests cross package boundaries:
// apps/web/tests/integration/user-flow.test.ts
import { createApp } from '@myrepo/api';
import { renderUserProfile } from '@myrepo/ui';
test('user profile renders real API data', async () => {
const app = createApp({ db: testDb });
const user = await app.users.create({ email: 'test@example.com' });
const { getByText } = renderUserProfile({ userId: user.id, client: app.client });
expect(getByText('test@example.com')).toBeInTheDocument();
});
These tests are slower and more complex but catch the bugs unit tests miss: mismatched TypeScript interfaces between packages, missing fields in API responses, incorrect client-side data handling.
TypeScript Project References and Testing
Monorepos with TypeScript project references require test runners that understand the reference graph. Vitest handles project references via resolve.tsconfig configuration. Jest requires moduleNameMapper entries per package — unmaintainable at 10+ packages. This is one of the practical reasons monorepos are migrating from Jest to Vitest: the TypeScript integration scales better.
For comprehensive test runner setup that works within monorepo constraints — including watch mode that re-runs only affected tests across packages when a source file changes — Vitest is the strongest choice. Its module graph awareness means vitest --watch in the root of a Turborepo monorepo identifies exactly which tests need to re-run without re-running the entire suite.
Remote Caching: The Multiplier Effect
Local caching eliminates redundant work on a single machine. Remote caching extends this to your entire team and CI infrastructure — the most impactful productivity unlock that monorepo tools provide.
How Remote Caching Works
When Turborepo completes a task (build, lint, test), it computes a cache key from the task's inputs: source files, environment variables declared in globalEnv, task configuration, and the dependency graph. It stores the outputs (built files, test results) keyed to that hash in a cache backend.
On the next run — whether on your machine or a CI worker — Turborepo computes the same hash. If it matches a cached entry, it downloads the outputs and replays them without executing. From your perspective, the task "ran" in under a second; in reality, another team member or CI run already did the work.
Turborepo Remote Cache Options
Turborepo's remote cache is built into Vercel's platform. If you host on Vercel, enabling remote caching is a single --team flag in your CI configuration. The Vercel cache is free within reasonable limits for Vercel-hosted projects.
For teams not on Vercel, self-hosted options exist: ducktape (an S3-compatible Turborepo cache server), turborepo-remote-cache (open source, deployable on Railway or Fly), or enterprise options like Nx Cloud (which also caches Turborepo tasks via API compatibility). The S3 backend is the simplest self-hosted option:
Cache Hit Rates in Practice
Cache hit rates vary significantly based on configuration quality. Common causes of unexpectedly low hit rates:
Environment variables not declared in globalEnv create separate caches per environment. If DATABASE_URL varies between developers but isn't declared (it shouldn't affect build outputs), Turborepo sees different inputs and creates separate cache entries.
Missing outputs declarations cause Turborepo to not restore cached build artifacts. A task that builds TypeScript but only declares dist/** in outputs while the actual output is build/** produces a "cached" run that doesn't restore the files you need.
Source files in unusual locations that match the task's input pattern but aren't meaningful (auto-generated files, editor artifacts) invalidate cache entries unnecessarily. Configure .gitignore-aligned exclusion patterns in your turbo.json inputs.
Nx Cloud vs Turborepo Remote Cache
Nx Cloud provides remote caching for both Nx tasks and Turborepo tasks (via a compatible API) with a generous free tier. The Nx Cloud dashboard shows cache hit analytics, task execution history, and cost attribution per workspace. For teams with complex monorepos where understanding cache behavior is important, the analytics justify the Nx Cloud configuration overhead even for Turborepo-based repos.
The practical difference at startup scale: Turborepo with Vercel remote cache is zero-config if you're on Vercel. Nx Cloud requires setup but works across any hosting provider and provides better observability. Moon doesn't have a native remote cache in its current release — teams using Moon typically pair it with one of the above for remote caching.
Compare monorepo tool package health on PkgPulse. Related: How to Set Up a Monorepo with Turborepo 2026 and Best JavaScript Package Managers 2026.
See the live comparison
View turborepo vs. nx on PkgPulse →