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. 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