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

> Source: <https://dev.to/zny10289/monorepos-in-2026-turborepo-vs-nx-vs-bazel-what-actually-works-1j85>
> Published: 2026-05-23 20:44:55+00:00

# Monorepos in 2026: What Actually Works

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**— One`git clone`

, one`npm 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

``` bash
# First build (slow)
$ 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)

# Second build (instant - cache hit)
$ 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

```
# Create Nx workspace
npx create-nx-workspace@latest myorg --preset=ts

# Add a new application
npx nx generate @nx/nextjs:app admin

# Add a library
npx nx generate @nx/js:library api-client --directory=packages/api-client
```

### The Affected Command (Nx's Killer Feature)

```
# Only build/test/lint what changed (and what depends on what changed)
npx nx affected --target=build --base=origin/main

# This is the game-changer for large monorepos
# Instead of rebuilding everything, Nx computes the dependency graph
# and only rebuilds the affected packages
```

### Visualizing the Project Graph

```
# Opens browser with interactive dependency 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**

```
# Move one package
mv ../old-repo/packages/ui apps/ui
cd apps/ui
npm init -y
# Update package.json name to @myorg/ui
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**

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

# packages/config-typescript/base.json
{
  "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:

```
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

# .npmrc
save-exact=true
```

### The Big Git History Question

Do you keep git history when migrating?

```
# Option 1: Keep history (preserve all commit messages)
git subtree add --prefix=apps/web https://github.com/old/web.git main

# Option 2: Fresh start (cleaner, lose history)
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

```
# GitHub Actions with Turborepo
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*
