Skip to main content

Turborepo vs Nx in 2026: Monorepo Tooling After Two Years

·PkgPulse Team

TL;DR

Turborepo wins for simplicity; Nx wins for large enterprise monorepos with complex constraints. If you're a startup or mid-size team that wants fast, cached builds with minimal configuration overhead — Turborepo is the right call. It's simpler to understand, integrates naturally into any package.json workflow, and the caching "just works." Nx has a steeper learning curve but pays off at scale: project graph visualization, enforced module boundaries, code generation, and team of 30+ workflows all favor Nx. For most teams: start with Turborepo. If you find yourself needing Nx's features, the migration is straightforward.

Key Takeaways

  • Turborepo: simpler setup, better for "I just need caching and task orchestration"
  • Nx: more opinionated, powerful project graph, enforced boundaries, better code generation
  • Remote caching: both offer cloud caching; Turborepo via Vercel (paid), Nx Cloud (free tier)
  • Bundle: Turborepo ~2MB Rust binary; Nx is Node.js-based
  • Download trends: both growing; Nx has more downloads (~4M/week vs ~2M for Turborepo)

What Each Tool Does

Turborepo (Vercel, 2021):
→ Task runner with intelligent caching
→ Works on top of existing npm/pnpm/yarn workspaces
→ turbo.json defines tasks and their dependencies
→ Local + remote caching (cache outputs, skip unchanged work)
→ Pipeline visualization
→ Relatively minimal — doesn't prescribe project structure

Nx (Nrwl, 2019 as "nx", earlier as angular-cli extensions):
→ Full monorepo framework
→ Project graph: understands all imports/dependencies between packages
→ Generators: create apps, libraries, components with `nx g`
→ Executors: custom build/test/serve targets per project type
→ Module boundary enforcement: "library X cannot import from Y"
→ Affected commands: `nx affected:test` only tests what changed
→ NX Cloud: remote caching + distributed task execution
→ Opinionated workspace structure

Key conceptual difference:
Turborepo: "cache the outputs of your existing scripts"
Nx: "understand your entire workspace and optimize based on that knowledge"

Setup and Configuration

// ─── Turborepo setup ───
// 1. Add turbo to existing monorepo:
// package.json (root):
{
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "workspaces": ["apps/*", "packages/*"]
}

// 2. turbo.json — define the task pipeline:
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],  // build deps first
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "tests/**"]
    },
    "lint": {
      "inputs": ["src/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

// 3. Run tasks:
// turbo build -- builds all packages, respecting dependencies, caching outputs
// turbo build --filter=@myapp/web -- build only web app and its deps
// turbo test --filter=[HEAD^1] -- test only changed since last commit
// ─── Nx setup ───
// nx.json (root config):
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["default", "^default"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/src/tests/**/*"]
  }
}

// project.json (per-project):
{
  "name": "web-app",
  "tags": ["scope:web", "type:app"],
  "targets": {
    "build": {
      "executor": "@nx/next:build",
      "options": { "outputPath": "dist/apps/web-app" }
    }
  }
}

// Run tasks:
// nx build web-app -- build with cache
// nx affected --target=test -- test only affected by current changes
// nx graph -- visual dependency graph in browser

Caching: The Core Value

# Both tools hash inputs and cache outputs

# Turborepo cache hit:
turbo build
# cache hit, replaying logs  packages/ui:build
# cache hit, replaying logs  packages/utils:build
# cache miss                 apps/web:build  ← only this runs
# Total time: 2.3s (would have been 45s without cache)

# Remote caching — Turborepo (Vercel Remote Cache):
# package.json:
{
  "scripts": {
    "build": "turbo build --token=$TURBO_TOKEN --team=myteam"
  }
}
# Free tier: limited
# Paid tier: unlimited cache storage + team sharing

# Remote caching — Nx Cloud:
# nx.json:
{
  "nxCloudId": "your-workspace-id"
}
# Free tier: generous (enough for most teams)
# Paid: higher limits + distributed task execution (DTX)

# Distributed task execution (Nx Enterprise feature):
# nx run-many --target=test --all -- runs tests across multiple machines
# Nx orchestrates which machine runs which test
# Turborepo doesn't have equivalent (local parallelism only)

# Speed comparison (500-file monorepo, warm cache):
# Turborepo: ~1.5s (Rust binary, fast startup)
# Nx: ~3s (Node.js, slightly slower startup)
# Cold cache: similar — depends on actual work being cached

Module Boundary Enforcement (Nx Only)

// Nx's "module boundaries" enforce architectural rules:

// .eslintrc.json:
{
  "plugins": ["@nx/eslint-plugin"],
  "rules": {
    "@nx/enforce-module-boundaries": ["error", {
      "allow": [],
      "depConstraints": [
        {
          // Apps can only depend on libraries, not other apps
          "sourceTag": "type:app",
          "onlyDependOnLibsWithTags": ["type:lib", "type:util"]
        },
        {
          // Data access libraries cannot depend on UI libraries
          "sourceTag": "type:data-access",
          "notDependOnLibsWithTags": ["type:ui"]
        },
        {
          // Shared utilities can only depend on other utilities
          "sourceTag": "scope:shared",
          "onlyDependOnLibsWithTags": ["scope:shared"]
        }
      ]
    }]
  }
}

// Tag your projects in project.json:
// apps/checkout: tags: ["scope:checkout", "type:app"]
// libs/ui: tags: ["scope:shared", "type:ui"]
// libs/auth-data: tags: ["scope:auth", "type:data-access"]

// Now if checkout app tries to import from billing app:
import { BillingService } from '@myapp/billing'; // ← ESLint error!
// Error: "A project tagged with 'type:app' can only depend on libs"

// This prevents:
// → "Big ball of mud" architectures where everything imports everything
// → Circular dependencies between domains
// → Business logic leaking into UI layers

// Turborepo doesn't have this — it's just a task runner
// For enforcement in Turborepo: add eslint-plugin-import/no-cycle + manual rules

Project Graph (Nx) and When It Matters

# Nx builds a complete dependency graph:
nx graph
# Opens browser at localhost:4211 with interactive graph:
# → All projects as nodes
# → All dependencies as edges
# → Highlight affected projects from a change
# → Filter to show subsets

# Affected commands — only run what's impacted:
nx affected --target=build --base=main --head=HEAD
# Analyzes git diff → maps files to projects → builds dependency graph
# → Builds only projects affected by the changes
# → Skips unchanged packages that would pass anyway

# Example: you changed libs/auth/src/jwt.ts
# Nx knows:
# → libs/auth is directly changed
# → apps/web depends on libs/auth (must rebuild)
# → apps/mobile depends on libs/auth (must rebuild)
# → apps/marketing does NOT depend on libs/auth (skip)
# → libs/payments depends on libs/auth (must rebuild)

# Turborepo equivalent:
turbo build --filter=[HEAD^1]  # or --filter=...[main]
# Similar concept, but less granular — works at file hash level
# Nx's graph analysis is more precise for affected detection

When to Choose Each

Choose Turborepo when:
→ You want faster builds with minimal new concepts to learn
→ Your team already understands npm workspaces — just add caching
→ You're a startup moving fast (turbo.json is ~20 lines to start)
→ You don't need module boundary enforcement or code generation
→ You prefer a minimal tool that does one thing well

Choose Nx when:
→ You have 5+ teams working in the same monorepo
→ You need enforced architectural boundaries between domains
→ You want code generation (nx generate @nx/react:component)
→ Distributed task execution across CI machines matters
→ You're building multiple app types (React, Node, mobile) in one repo
→ The visual dependency graph would help your team

The migration path:
→ Start with Turborepo
→ If you find yourself manually enforcing module boundaries: add Nx
→ Nx can run alongside Turborepo; many teams use Nx Cloud for caching with Turborepo

Real recommendation (2026):
→ 1-3 devs: neither — npm workspaces with a Makefile is enough
→ 3-15 devs: Turborepo — fast, simple, effective
→ 15-50 devs: Turborepo with Nx Cloud caching OR Nx
→ 50+ devs: Nx — the architectural governance features justify the complexity

Compare Turborepo and Nx download trends at PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.