cd /news/developer-tools/monorepos-in-2026-turborepo-vs-nx-vs… · home topics developer-tools article
[ARTICLE · art-12398] src=dev.to pub= topic=developer-tools verified=true sentiment=· neutral

Monorepos in 2026: Turborepo vs Nx vs Bazel — What Actually Works

The article summarizes the state of monorepo tools in 2026, concluding that Turborepo has become the default for most JavaScript/TypeScript teams due to its simplicity and sensible defaults, while Nx serves enterprises needing complex dependency graph management and Bazel remains the choice for Google-scale projects. It notes that monorepos became standard because they eliminate the productivity loss of managing multiple repositories, and provides practical insights from running all three tools in production. The author emphasizes that for most teams, Bazel is overkill, and the key tradeoffs are between Turborepo's ease of use and Nx's advanced features like affected commands and distributed caching.

read6 min views5 publishedMay 23, 2026

The monorepo debate in 2026 has settled into something more mature. Turborepo became the default for most JS/TS teams, Nx found its niche in enterprises with complex dependency graphs, and Bazel still dominates at Google scale. Here's what I've learned from running all three in production.

Why Monorepos Won #

Let's be clear about why monorepos became the default in 2026:

Atomic commits across services— Change an API contract and update all consumers in one PR - Shared tooling— One ESLint config, one Prettier config, one TypeScript config - Easy refactoring— Rename a function used across 20 packages without coordination - Unified CI/CD— One pipeline that understands dependency relationships - Developer experience— Onegit clone

, onenpm install

, everything works

The alternative — polyrepo hell where a simple rename requires 15 PRs across 15 repos — killed productivity. Teams migrated.

Turborepo: The Default Choice #

Turborepo won because it has sensible defaults, a gentle learning curve, and just works for most JavaScript/TypeScript projects. It's not the most powerful, but it's the most practical.

Setting Up a Turborepo Project in 2026

// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.4.0"
  }
}
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    }
  }
}

Real Package Dependencies

apps/
  web/          → depends on @repo/ui, @repo/api-client
  admin/        → depends on @repo/ui, @repo/api-client
  docs/         → depends on @repo/ui, @repo/docs-utils
packages/
  ui/           → no internal deps
  api-client/   → no internal deps
  docs-utils/   → no internal deps
  config/       → no internal deps (ESLint, TypeScript configs)

The Cache That Actually Works

$ turbo build
• building web
• building admin
• building docs
• building ui
• building api-client
• building docs-utils

→ Tasks run by turbo: 6 (64ms each, 384ms total)

$ turbo build
• building web (cache hit)
• building admin (cache hit)
• building docs (cache hit)
• building ui (cache hit)
• building api-client (cache hit)
• building docs-utils (cache hit)

→ Tasks run by turbo: 0 (6 in cache, 12ms total)

Nx: When You Need the Power #

Nx is Turborepo on steroids. It understands your project graph at a deeper level, has built-in code generators, affected commands, and powerful visualization tools. The tradeoff: more complexity, steeper learning curve.

Nx Project Setup

npx create-nx-workspace@latest myorg --preset=ts

npx nx generate @nx/nextjs:app admin

npx nx generate @nx/js:library api-client --directory=packages/api-client

The Affected Command (Nx's Killer Feature)

npx nx affected --target=build --base=origin/main

Visualizing the Project Graph

npx nx graph

This generates a visual graph of your entire project. You can see exactly which packages depend on which, spot circular dependencies, and understand the impact of proposed changes.

Nx Computation Cache (Distributed)

// nx.json
{
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": ["!{projectRoot}/**/*.spec.ts"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true,
      "inputs": ["production", "^production"]
    }
  },
  "nxCloudAccessToken": "your-nx-cloud-token"
}

Nx Cloud provides distributed caching — your CI server and local machine share the same build cache. Cold CI builds become warm builds instantly.

Bazel: At Google Scale #

Bazel is the nuclear option. It's infinitely scalable, hermetically sealed (reproducible builds), and can handle truly massive codebases. The tradeoff: significant operational overhead, Starlark learning curve, and ecosystem fragmentation outside the Google ecosystem.

When Bazel Makes Sense

  • 1,000+ engineers in a single codebase
  • Multiple languages (TypeScript + Python + Go + Rust + Java)
  • Need per-language optimized tooling
  • hermetic builds are a hard requirement
  • You're okay with dedicated build infrastructure team

For most teams, Bazel is overkill. The rule of thumb: if you're asking whether you need Bazel, you don't.

The Migration Path #

From Polyrepo to Turborepo

Step 1: Create the monorepo structure

mkdir my-monorepo && cd my-monorepo
git init
npm init -y
npm install -D turbo
mkdir apps packages

Step 2: Move packages one at a time

mv ../old-repo/packages/ui apps/ui
cd apps/ui
npm init -y
cd ../..
git add -A
git commit -m "feat: move ui package"

Step 3: Fix dependency issues

// Before (old package.json)
{
  "name": "ui",
  "dependencies": {
    "react": "^18.0.0"
  }
}

// After (in monorepo)
{
  "name": "@myorg/ui",
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

Step 4: Add shared configs

module.exports = {
  extends: ['next/core-web-vitals', 'turbo'],
  rules: {
    '@typescript-eslint/no-unused-vars': 'error'
  }
};

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

The Hidden Costs Nobody Warns You About #

Build Tool Lockstep

When you share a config

package across 20 apps, updating TypeScript becomes a coordinated event. You can't have App A on TypeScript 5.3 and App B on 5.4 while they're in the same repo.

Solution: Use pnpm workspaces

with version pinning:

packages:
  - 'apps/*'
  - 'packages/*'

save-exact=true

The Big Git History Question

Do you keep git history when migrating?

git subtree add --prefix=apps/web https://github.com/old/web.git main

mv old-repo/web apps/web

I recommend Option 2 for most migrations. Git history for individual packages in a monorepo is rarely useful, and the migration complexity isn't worth it.

CI/CD Complexity

name: CI

on: [push, pull_request]

jobs:
  affected:
    name: Check affected
    runs-on: ubuntu-latest
    outputs:
      affected: ${{ steps.turbo.outputs.affected }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - id: turbo
        run: echo "affected=$(pnpm turbo run build --dry-run=json | jq -r '.packages | if length > 0 then "true" else "false" end')" >> $GITHUB_OUTPUT

  build:
    needs: affected
    if: needs.affected.outputs.affected == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo build --filter=...affected packages...

2026 Tooling Ecosystem #

Tool Language Agnostic Learning Curve Scale Best For
Turborepo JS/TS mostly Gentle < 100 packages Most teams
Nx Multiple Medium < 500 packages Enterprises
Bazel Any Steep Unlimited Google-scale
Lerna JS/TS Gentle < 50 packages Legacy (deprecated)

My Recommendation #

For most teams in 2026: Start with Turborepo. It's the right default. You get 80% of the benefit for 20% of the complexity.

If you're in an enterprise with complex dependencies, code generators, and a dedicated platform team: Nx is worth the investment.

If you're at Google scale: Bazel. But you already knew that.

The worst choice: using no monorepo tooling at all. Running a monorepo with just npm workspaces

and manual dependency tracking is technical debt that compounds.

Running a monorepo? What's your tool of choice in 2026? Let's hear real-world experiences.

Further reading: Turborepo docs, Nx docs, Bazel migration guide

── more in #developer-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/monorepos-in-2026-tu…] indexed:0 read:6min 2026-05-23 ·