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, 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
# 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
Top comments (0)