Monorepo: Why and How (Nx, Turborepo, Tradeoffs)
A monorepo puts all your code in one repository. Done right, it speeds up development, enforces boundaries, and makes refactoring across projects trivial. Here's the real picture.
The pendulum in software architecture swings between two extremes: everything in one place, and everything split apart. Monorepos bring multiple projects into a single repository — and done right, they’re genuinely powerful. Done wrong, they’re a slow, tangled mess.
Let’s look at what monorepos actually offer, where they fall short, and which tooling makes them work at scale.
What Is a Monorepo?
A monorepo is a single version-controlled repository containing multiple projects. Those projects can be independent applications, shared libraries, or both.
my-company/
apps/
web/ ← React frontend
mobile/ ← React Native
api/ ← Node.js API
packages/
ui/ ← shared component library
utils/ ← shared utilities
config/ ← shared ESLint, tsconfig, etc.
package.json ← root workspace config
This is NOT a monolith. The apps are still independently deployable. The code just lives in one repo.
Why Teams Choose Monorepos
Atomic cross-project changes
In a polyrepo (one repo per project), updating a shared utility means:
- PR in
utilsrepo → merge → publish new version - PR in
webto upgrade the version - PR in
apito upgrade the version - Coordinate releases across 3 repos
In a monorepo, one PR changes the utility and all consumers at once. No version coordination. No stale dependency hell.
Shared tooling and standards
One .eslintrc, one tsconfig.base.json, one CI pipeline definition. Consistency is automatic.
Easier refactoring
When you rename a type that’s used across 5 packages, your IDE and TypeScript compiler see all of them. You refactor once, not five times.
Unified dependency management
One node_modules at the root (or near-root). No diverging versions of the same library across packages. Easier to audit for security issues.
The Tradeoffs
CI/CD complexity
If every PR triggers a full build and test of all projects, CI gets slow fast. Monorepos require incremental builds — only rebuild what changed.
Repository size
Over years, a monorepo can grow very large. Git operations slow down. You need shallow clones and sparse checkout for developer machines.
Access control
Some teams need different access controls per project (contractors who can see the frontend but not the API). Monorepos make this harder.
Tooling investment
Monorepos require dedicated tooling. Without it, they’re chaos. With it, they’re powerful.
Turborepo
Turborepo (by Vercel) is the simplest way to get monorepo task caching and parallel execution:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
# Install
npm install turbo --save-dev
# Build only affected packages (with cache)
npx turbo run build
# Test in parallel
npx turbo run test --parallel
Turborepo caches task outputs locally and remotely. If you haven’t changed packages/utils, its build output is restored from cache — not rebuilt.
Best for: frontend-focused monorepos, teams already using Vercel, projects with 2–20 packages.
Nx
Nx (by Nrwl) is more opinionated and more powerful. It has a full plugin ecosystem, generators, and a project graph that understands dependencies deeply.
# Create a new Nx workspace
npx create-nx-workspace@latest myorg --preset=ts
# Add apps and libraries
nx g @nx/react:app web
nx g @nx/node:app api
nx g @nx/js:lib utils
# Run only affected projects
nx affected --target=build
nx affected --target=test
Nx’s affected command is its killer feature: it analyzes the dependency graph and only runs tasks for projects affected by your changes.
Changed: packages/utils
Affected: apps/web (imports utils), apps/api (imports utils)
Not affected: apps/mobile (doesn't import utils)
→ Only build and test web and api
Project graph visualization:
nx graph # Opens browser with interactive dependency graph
Best for: larger teams, backend+frontend monorepos, projects needing generators and scaffolding, Angular-heavy projects (Nx’s origin).
Turborepo vs Nx
| Turborepo | Nx | |
|---|---|---|
| Learning curve | Low | Medium-high |
| Caching | ✅ Local + remote | ✅ Local + remote |
| Affected detection | Basic | Advanced (graph-based) |
| Generators | ❌ | ✅ |
| Plugin ecosystem | Small | Large |
| Framework opinions | None | Strong |
| Best for | Simple setups | Complex setups |
Workspace Configuration
Both tools use npm/yarn/pnpm workspaces underneath:
// package.json (root)
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
}
}
// packages/utils/package.json
{
"name": "@myorg/utils",
"version": "0.0.1",
"main": "./dist/index.js",
"scripts": {
"build": "tsc",
"test": "vitest"
}
}
// apps/web/package.json
{
"dependencies": {
"@myorg/utils": "*" // Reference local package by name
}
}
When to Use a Monorepo
Good fit:
- Multiple apps sharing significant code (design system, utilities, types)
- A platform team managing multiple services with shared contracts
- Teams doing frequent cross-project refactors
Poor fit:
- Independent projects with no shared code
- Teams with strict access control requirements per project
- Very small teams where the tooling overhead isn’t worth it
Key Takeaways
- Monorepos enable atomic cross-project changes, shared tooling, and easier refactoring
- They require investment in CI tooling — incremental builds are non-negotiable
- Turborepo is simpler and lower-overhead; Nx is more powerful and more opinionated
- Both provide caching and affected detection that make large monorepos fast
- A monorepo is not a monolith — projects remain independently deployable