React 119 ⚛️ Monorepo Management for React Projects (Turborepo / Nx)
A React monorepo is not a repository with multiple package.json files. It is a repository where the dependency graph between packages is known, where a change to a shared UI component triggers only the builds and tests that depend on it, and where the CI pipeline finishes in seconds rather than minutes because the cache is shared across machines. The tool that provides this—Turborepo or Nx—is the difference between a folder full of projects and a workspace that scales.
This chapter covers the two dominant monorepo tools for React projects: Turborepo and Nx. It begins with the problem they solve—task orchestration and caching across a dependency graph—and the package manager layer that both depend on. It then examines Turborepo’s minimal configuration and its remote cache, followed by Nx’s generator system, plugin ecosystem, and module boundary enforcement. The final sections address the decision between the two tools, the CI setup that makes affected builds practical, and the cases where a monorepo is the wrong choice entirely.
Key point: Turborepo and Nx are not package managers. They are task runners and caches that sit on top of pnpm, npm, or yarn workspaces. The package manager resolves dependencies; the monorepo tool decides what to run and when to skip it.
Why monorepo tooling exists
The affected-build problem. In a repository with ten packages, a change to a shared utility should not rebuild every application. The tool must know which packages depend on the changed one and run tasks only for those. Turborepo does this with --filter='[main]' and Nx with nx affected. Without this, CI runs everything on every commit, and the pipeline time grows linearly with the repository size .
The cache problem. A build that has already been run does not need to be run again. The tool hashes the inputs—source files, configuration, environment variables—and if the hash matches a previous run, it restores the output from cache instead of re-executing the task. Turborepo stores this cache locally and can share it remotely via Vercel or a self-hosted server. Nx offers Nx Cloud as a managed remote cache. On a real repository, the difference between a cold build and a cache hit is the difference between minutes and seconds .
The consistency problem. A new package in a monorepo should have the same lint configuration, the same TypeScript settings, and the same test setup as the existing packages. Nx addresses this with generators that scaffold a complete, passing project in one command. Turborepo leaves the scaffolding to the developer or to a template like create-turbo . The consistency requirement grows with the team size; at five developers, it is a convenience; at fifty, it is a necessity.
The boundary problem. In a large monorepo, the temptation to import from a sibling feature package is strong, and the resulting coupling is difficult to unwind. Nx provides an ESLint rule, @nx/enforce-module-boundaries, that makes the import fail at lint time. Turborepo has no equivalent; the boundary enforcement is left to code review or custom tooling .
a. Turborepo: minimal configuration and remote cache
Turborepo is configured through turbo.json, a file that defines the task pipeline. The pipeline declares which tasks depend on which other tasks, what the inputs and outputs are, and whether the task is cacheable. The ^build syntax means “run the build of every dependency before running this build,” which is how a shared UI package is built before the application that imports it .
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
The outputs field tells Turborepo what to cache after a successful build. The exclusion of .next/cache is deliberate: the framework’s internal build cache contains absolute paths and is not portable across machines. Only the final artifacts are cached . The dev task has cache: false because a development server never “completes,” and persistent: true because it runs indefinitely and should not block other tasks from starting.
The filtering syntax is the feature that makes Turborepo practical. turbo build --filter='[main]' runs the build only for packages that changed since the main branch. turbo build --filter=web runs the build for the web application and its dependencies. turbo build --filter=@repo/ui... runs the build for the UI package and everything that depends on it .
The remote cache is configured with turbo login and turbo link, which connect the repository to a Vercel account. Once linked, the cache is shared across the developer’s machine, CI, and every other developer. A build that hashes to the same inputs on a colleague’s machine is a cache hit on CI, and the cold-build time drops accordingly . For teams that do not use Vercel, the ducktors/turborepo-remote-cache package provides a self-hostable server .
The setup is intentionally minimal. create-turbo scaffolds a repository with two Next.js applications and a shared UI package, and the turbo.json is small enough to read in a minute . This minimalism is the tool’s selling point: there is no plugin system to learn, no generator to configure, and no project graph to manage beyond what the package manager already provides.
b. Nx: generators, plugins, and boundaries
Nx is a larger system. The create-nx-workspace command scaffolds a workspace with a preset—next, react, react-monorepo—and the @nx/react plugin provides generators for applications, libraries, components, and hooks . The generator for a library creates the directory, the project.json, the TypeScript configuration, the lint configuration, and a passing test setup in one command.
# Create a workspace with a Next.js preset.
pnpm dlx create-nx-workspace@latest my-workspace --preset=next
# Generate a React library with Vite as the bundler.
nx g @nx/react:library --name=ui --directory=libs/ui --bundler=vite
# Generate a Next.js application.
nx g @nx/next:application --name=dashboard --directory=apps/dashboard
# Generate a component inside the library.
nx g @nx/react:component --name=button --project=ui --export
The nx graph command opens a browser visualization of the project dependency graph. For a repository with a few packages, this is a convenience. For a repository with fifty or a hundred, it is the primary tool for understanding the impact of a change . The nx affected command runs tasks only for the projects that changed since a base branch, which is the same concept as Turborepo’s filter but integrated with the graph and the plugin system.
The module boundary enforcement is the feature that distinguishes Nx for large teams. The @nx/enforce-module-boundaries ESLint rule, combined with tags on each project, makes it a lint error for a feature library to import from another feature library. The rule encodes architectural decisions as enforceable constraints rather than documentation that decays .
Nx Cloud is the managed remote cache. It is more polished than self-hosting Turborepo’s cache, and it includes distributed task execution, which spreads a CI run across multiple machines. The cost is a subscription for larger teams, and the configuration is more involved than turbo login .
The trade-off is clear. Nx adds a plugin system, a generator system, a graph visualizer, and a boundary enforcement layer. Each of these solves a real problem at scale. Each of them also adds configuration, concepts, and maintenance overhead. The Nx documentation itself suggests that for “two Next.js apps, three shared packages, five developers,” Turborepo is the more appropriate choice .
c. The package manager layer and the decision
Both Turborepo and Nx sit on top of a package manager’s workspace feature. pnpm is the common recommendation for monorepos because its content-addressable store avoids duplicating dependencies across packages, and its strict node_modules layout prevents phantom dependencies . The workspace:* protocol in package.json links internal packages without publishing them, and the package manager resolves the graph that the monorepo tool then orchestrates.
// apps/web/package.json
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*"
}
}
The decision between Turborepo and Nx follows from the repository’s size and the team’s requirements. Turborepo is the right choice for a small to medium repository—a few applications, a handful of shared packages, a team that values minimal configuration. The remote cache is a single command, the pipeline file is short, and the mental model is a task runner with a cache. Nx is the right choice when the repository is large, when the team is large, when consistency across many packages matters, or when the architecture requires enforced boundaries. The generators and the graph visualizer pay for their overhead at scale .
A third option exists for teams that want the cache without the full framework: nx init adds Nx to an existing pnpm or npm workspace without converting it to the Nx project structure. The command detects the existing scripts, builds a project graph, and enables caching and affected builds. The plugins and generators remain optional . This is the pragmatic path for a repository that has outgrown manual orchestration but does not want to adopt the full Nx model.
Complete Example Session
# ============================================
# PART 1: THE TURBOREPO SETUP
# ============================================
# Scaffold a repository with two Next.js apps
# and a shared UI package.
pnpm dlx create-turbo@latest my-monorepo
cd my-monorepo
# ============================================
# PART 2: THE WORKSPACE STRUCTURE
# ============================================
# apps/web Next.js application
# apps/docs Next.js documentation
# packages/ui Shared React components
# packages/eslint-config Shared lint config
# packages/typescript-config Shared tsconfig
# turbo.json Task pipeline
# ============================================
# PART 3: THE TURBO PIPELINE
# ============================================
# build depends on the build of dependencies.
# outputs are cached; dev is not cached.
cat turbo.json
# ============================================
# PART 4: THE FILTER COMMANDS
# ============================================
# Only build what changed since main.
turbo build --filter='[main]'
# Build a specific app and its dependencies.
turbo build --filter=web
# Build a package and everything that depends on it.
turbo build --filter=@repo/ui...
# ============================================
# PART 5: THE REMOTE CACHE
# ============================================
# Link to Vercel for shared caching.
turbo login
turbo link
# CI environment variables:
# TURBO_TOKEN=<vercel-token>
# TURBO_TEAM=<team-slug>
# ============================================
# PART 6: THE NX WORKSPACE
# ============================================
# Scaffold with the Next.js preset.
pnpm dlx create-nx-workspace@latest my-nx-workspace --preset=next
# ============================================
# PART 7: THE NX GENERATORS
# ============================================
# Generate a library, an app, and a component.
nx g @nx/react:library --name=ui --directory=libs/ui --bundler=vite
nx g @nx/next:application --name=dashboard --directory=apps/dashboard
nx g @nx/react:component --name=button --project=ui --export
# ============================================
# PART 8: THE AFFECTED COMMANDS
# ============================================
# Run tasks only for changed projects.
nx affected -t lint
nx affected -t test --base=main
nx affected -t build --parallel=4
# ============================================
# PART 9: THE GRAPH AND BOUNDARIES
# ============================================
# Visualize the dependency graph.
nx graph
# Enforce module boundaries with tags and ESLint.
# The rule is @nx/enforce-module-boundaries.
# ============================================
# PART 10: THE CI PIPELINE
# ============================================
# Turborepo + GitHub Actions:
# - run: pnpm turbo build --filter='[origin/main]'
#
# Nx + GitHub Actions:
# - run: pnpm exec nx affected -t lint test build --parallel=3
echo "Monorepo setup complete."
The ten parts walk through both tools from scaffolding to CI. The key observation is that the package manager’s workspace feature provides the dependency resolution, and the monorepo tool provides the orchestration and caching. Neither replaces the other.
Quick Reference
Tool Comparison
| Feature | Turborepo | Nx |
|---|---|---|
| Configuration | turbo.json (minimal) | nx.json + project.json per project |
| Task runner | Yes | Yes |
| Local cache | Yes | Yes |
| Remote cache | Vercel / self-hosted | Nx Cloud |
| Generators | No (templates only) | Extensive plugin ecosystem |
| Graph visualizer | --graph flag | nx graph |
| Affected builds | --filter='[main]' | nx affected |
| Module boundaries | No | ESLint rule |
| Best for | Small to medium repos | Large, polyglot, strict boundaries |
Turborepo Filters
| Filter | Effect |
|---|---|
--filter='[main]' | Changed since main branch |
--filter=web | Specific app and its deps |
--filter=@repo/ui... | Package and dependents |
--filter='!docs' | Everything except docs |
Nx Commands
| Command | Purpose |
|---|---|
nx graph | Visualize dependency graph |
nx affected -t test | Test only changed projects |
nx g @nx/react:lib | Generate a React library |
nx g @nx/react:component | Generate a component |
Package Manager Layer
| Feature | Tool |
|---|---|
| Dependency resolution | pnpm / npm / yarn |
| Workspace linking | workspace:* protocol |
| Content-addressable store | pnpm |
| Task orchestration | Turborepo / Nx |
Best Practices
✅ Do This:
# Use pnpm for the workspace layer.
pnpm dlx create-turbo@latest # ✅
# Configure the pipeline before adding packages.
cat turbo.json # define build, lint, test, dev # ✅
# Use affected filters in CI.
turbo build --filter='[origin/main]' # ✅
# Link the remote cache for team sharing.
turbo login && turbo link # ✅
# Use generators for consistency in Nx.
nx g @nx/react:library --name=ui # ✅
# Enforce boundaries in Nx with tags.
@nx/enforce-module-boundaries # ✅
❌ Don’t Do This:
# Don't treat Turborepo as a package manager.
turbo install # does not exist; use pnpm install // ❌
# Don't cache the dev server.
"dev": { "cache": true } // the server never completes // ❌
# Don't cache framework-internal build caches.
"outputs": [".next/**"] // includes .next/cache, not portable // ❌
# Don't adopt Nx for a two-app repository.
# The generator overhead is not justified at that scale. // ❌
# Don't skip the package manager workspace.
# The monorepo tool needs the dependency graph to orchestrate. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Cache misses on CI | Remote cache not linked | turbo link, set TURBO_TOKEN |
| Stale builds | Inputs not declared in pipeline | Add inputs to turbo.json |
| Dev server blocks tasks | persistent: false | Set persistent: true |
| Boundary violation | No lint rule | Enable @nx/enforce-module-boundaries |
| Phantom dependencies | npm/yarn hoisting | Use pnpm with strict layout |
| Generator not found | Nx plugin version mismatch | nx add @nx/react to sync versions |
Real-World Examples
1. Turborepo Filter
turbo build --filter='[main]'
2. Turborepo Remote Cache
turbo login && turbo link
3. Turborepo Pipeline
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }
4. Nx Generate Library
nx g @nx/react:library --name=ui --directory=libs/ui
5. Nx Affected
nx affected -t test --base=main
6. Nx Graph
nx graph
7. Nx Init on Existing Workspace
nx init
8. Workspace Protocol
"@repo/ui": "workspace:*"
9. Persistent Dev Task
"dev": { "cache": false, "persistent": true }
10. CI Affected Build
pnpm turbo build --filter='[origin/main]'
Visual
The Monorepo Layer Stack
┌─────────────────────────────────────────────────────────┐
│ TURBOREPO / NX │
│ Task orchestration, caching, affected builds │
│ │ │
│ ▼ │
│ PACKAGE MANAGER (pnpm / npm / yarn) │
│ Dependency resolution, workspace linking │
│ │ │
│ ▼ │
│ PROJECTS │
│ apps/web, apps/docs, packages/ui, packages/utils │
│ │
│ The monorepo tool does not install dependencies. │
│ The package manager does not decide what to run. │
└─────────────────────────────────────────────────────────┘
Turborepo Task Pipeline
┌─────────────────────────────────────────────────────────┐
│ turbo build │
│ │ │
│ ▼ dependsOn: ["^build"] │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 1. Build packages/ui (dependency) │ │
│ │ 2. Build apps/web (depends on ui) │ │
│ │ 3. Build apps/docs (depends on ui) │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ▼ hash inputs │
│ Cache hit? → restore outputs, skip task │
│ Cache miss? → run task, store outputs │
└─────────────────────────────────────────────────────────┘
The Decision Matrix
┌─────────────────────────────────────────────────────────┐
│ Repository size and team size │
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ Small Large │
│ (2-5 apps) (10+ apps) │
│ │ │ │
│ ▼ ▼ │
│ Turborepo Nx │
│ minimal generators, graph, boundaries │
│ config plugin ecosystem │
│ │
│ Polyglot stack (React + Node + Python)? │
│ → Nx has plugins for many ecosystems. │
│ │
│ Strict module boundaries required? │
│ → Nx has the ESLint rule. │
└─────────────────────────────────────────────────────────┘
The Affected Build
┌─────────────────────────────────────────────────────────┐
│ Change: packages/ui/src/button.tsx │
│ │ │
│ ▼ │
│ Dependency graph: │
│ packages/ui ← apps/web │
│ packages/ui ← apps/docs │
│ │
│ Affected: packages/ui, apps/web, apps/docs │
│ Unaffected: packages/utils, packages/config │
│ │
│ turbo build --filter='[main]' │
│ → builds only the three affected projects │
│ → skips utils and config │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Package manager | pnpm (recommended), npm, yarn |
| Turborepo config | turbo.json |
| Turborepo cache | Local + Vercel / self-hosted |
| Turborepo filter | --filter='[main]' |
| Nx config | nx.json + project.json |
| Nx generators | @nx/react, @nx/next, etc. |
| Nx affected | nx affected -t task |
| Nx boundaries | @nx/enforce-module-boundaries |
| Nx remote cache | Nx Cloud |
| Small repo choice | Turborepo |
| Large repo choice | Nx |
Key takeaways:
- The monorepo tool is not the package manager. Turborepo and Nx orchestrate tasks and cache results on top of pnpm, npm, or yarn workspaces. The package manager resolves dependencies; the tool decides what to run .
- Turborepo is minimal by design. The
turbo.jsonpipeline is short, the remote cache is a single login command, and the filter syntax is the primary interface. It is the right choice for small to medium repositories . - Nx is a platform. Generators scaffold consistent projects, the graph visualizer shows dependencies, and the boundary rule enforces architecture. The overhead pays for itself at scale, not before .
- The remote cache is the CI accelerator. A cache hit restores outputs instead of re-running the task. Linking the cache across developer machines and CI is the difference between minutes and seconds on a cold build .
- Affected builds are the other half of the speed. Running tasks only for changed packages requires knowing the dependency graph. Both tools provide this, with different syntax and different integration depth.
- The
devtask must be persistent and uncached. A development server never completes, and caching it makes no sense. Thepersistent: trueflag tells the orchestrator not to wait for it . nx initadds caching to an existing workspace. A repository that has outgrown manual orchestration but does not want the full Nx model can adopt the cache and affected commands without the generators or the project structure .
Remember: A monorepo is a dependency graph, and the tool’s job is to know that graph. Turborepo and Nx both provide task orchestration, caching, and affected builds; the difference is how much framework surrounds those core features. Turborepo is a task runner with a cache and a filter. Nx is a workspace platform with generators, plugins, and boundary enforcement. Choose the tool that matches the repository’s size and the team’s consistency requirements, and remember that the package manager’s workspace is the foundation both tools depend on.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!