Monorepo — The Complete Guide to Managing Multiple Projects in One Repository
Introduction: Why Monorepos Are Taking Over
Monorepos are increasingly popular in modern software development — and for good reason. Major companies like Google, Meta, Microsoft, and large open-source projects such as Babel, Jest, and React all use a monorepo approach. But what exactly is a monorepo, and why should you consider one for your team?
In this comprehensive guide, we’ll cover everything from the fundamentals to the tools that make monorepo development a delight.
Table of Contents
- Introduction
- What is a Monorepo?
- Monorepo vs. Monolith
- Polyrepo: The Traditional Approach
- Why Choose a Monorepo?
- Key Features of a Monorepo
- Monorepo Tools Compared
- Choosing the Right Tool
- Getting Started: Monorepo with Nx
- Getting Started: Monorepo with Turborepo
- Best Practices
- Conclusion
What is a Monorepo?
A monorepo is a single repository containing multiple distinct projects, with well-defined relationships between them.
The key phrase here is “well-defined relationships.” A monorepo is not just dumping all your code into a single repository. Each project within the monorepo should be a discrete unit with clear boundaries, dependencies, and ownership.
What a Monorepo Is Not
- Not just “code colocation” — If you have several projects in a repo but no well-defined relationships, that’s just a folder with code, not a monorepo.
- Not a monolith — If a repo contains a massive application without division into encapsulated parts, it’s just a big (monolithic) repo. A good monorepo is the opposite of monolithic.
# A well-structured monorepo
my-monorepo/
├── apps/
│ ├── web-app/ # React frontend
│ ├── mobile-app/ # React Native app
│ └── api-server/ # Node.js backend
├── packages/
│ ├── ui-components/ # Shared UI library
│ ├── utils/ # Shared utilities
│ └── config/ # Shared configs (ESLint, TypeScript, etc.)
├── package.json
├── nx.json # or turbo.json
└── README.md
Monorepo Structure Diagram
graph TD
A[Monorepo Root] --> B[apps/]
A --> C[packages/]
A --> D[tools/]
A --> E[configs/]
B --> F[web-app]
B --> G[mobile-app]
B --> H[api-server]
C --> I[ui-components]
C --> J[utils]
C --> K[config-eslint]
C --> L[config-typescript]
F --> I
F --> J
G --> I
G --> J
H --> J
H --> K
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fce4ec
Monorepo vs. Monolith
This is one of the most common misconceptions. Let’s set the record straight:
| Aspect | Monorepo | Monolith |
|---|---|---|
| Structure | Multiple distinct projects in one repo | One tightly-coupled application |
| Boundaries | Clear module boundaries and APIs | Blurred boundaries between components |
| Deployment | Independent deployment per project | Single deployment unit |
| Scalability | Scales with proper tooling | Becomes harder to scale |
| Team Ownership | Each team owns their projects | Single team or shared ownership |
A good monorepo actually promotes modularity by making it easy to share code while keeping projects independent and deployable on their own.
Polyrepo: The Traditional Approach
A polyrepo (the opposite of a monorepo) is the standard approach where each team, application, or library has its own repository. While polyrepos offer team autonomy, that autonomy comes through isolation, and isolation hurts collaboration.
Common Polyrepo Problems
1. Cumbersome Code Sharing
To share code across repositories, you need to create a separate repo for shared code, set up CI/CD, configure package publishing, and manage versioning. It’s a significant overhead.
# Polyrepo: sharing code is painful
team-a-repo/ → publishes @company/shared-utils v1.2.0
team-b-repo/ → depends on @company/shared-utils v1.1.0 😬
team-c-repo/ → depends on @company/shared-utils v1.0.0 😱
2. Significant Code Duplication
Because setting up shared repos is so tedious, teams often rewrite common utilities in each repo — wasting time and increasing maintenance burden.
3. Costly Cross-Repo Changes
A critical bug in a shared library? You’ll need to fix it in the library repo, publish a new version, then update and test every consumer repo separately. Coordinating versions across repos is a nightmare.
4. Inconsistent Tooling
Each project uses its own set of commands for testing, building, linting, and deploying. This creates mental overhead as developers switch between projects.
Why Choose a Monorepo?
A monorepo solves these polyrepo pain points elegantly:
Polyrepo vs Monorepo Workflow Comparison
flowchart TD
subgraph "Polyrepo Workflow"
A1[Fix shared library bug] --> B1[Update library in lib-repo]
B1 --> C1[Publish new version v2.1.0]
C1 --> D1[Update consumer-repo-1 to v2.1.0]
C1 --> E1[Update consumer-repo-2 to v2.1.0]
C1 --> F1[Update consumer-repo-3 to v2.1.0]
D1 --> G1[Test consumer-repo-1]
E1 --> H1[Test consumer-repo-2]
F1 --> I1[Test consumer-repo-3]
G1 --> J1[Deploy consumer-repo-1]
H1 --> K1[Deploy consumer-repo-2]
I1 --> L1[Deploy consumer-repo-3]
end
subgraph "Monorepo Workflow"
A2[Fix shared library bug] --> B2[Update library in packages/]
B2 --> C2[Commit all changes together]
C2 --> D2[Run affected tests]
D2 --> E2[Deploy all affected apps]
end
style A1 fill:#ffebee
style A2 fill:#e8f5e8
style B1 fill:#ffebee
style B2 fill:#e8f5e8
No Overhead to Create New Projects
Use the existing CI setup. No need to publish versioned packages when all consumers are in the same repo.
# With Nx: create a new app in seconds
npx nx g @nx/react:app my-new-app
# With Turborepo: just add a new folder
mkdir apps/my-new-app
Atomic Commits Across Projects
Everything works together at every commit. Fix a shared library and all its consumers in the same commit — no breaking changes, no version coordination.
# One commit that updates shared lib + all consumers
git commit -m "fix: update validation logic across all apps"
One Version of Everything
No more incompatible versions of third-party libraries across projects. A single package.json (or a root dependency manager) ensures consistency.
Developer Mobility
Developers get a consistent way of building and testing across different tools and technologies. They can confidently contribute to other teams’ projects and verify their changes are safe.
Key Features of a Monorepo
When choosing a monorepo tool, here are the features to look for:
⚡ Speed (Fast)
Local Computation Caching
Store and replay file and process outputs. Never build or test the same thing twice on the same machine.
# First run: builds everything
npx nx build my-app # Takes 45 seconds
# Second run: restored from cache
npx nx build my-app # Takes 0.5 seconds ⚡
Local Task Orchestration
Run tasks in the correct order and in parallel, respecting the dependency graph.
# Nx runs tasks in parallel, respecting dependencies
npx nx run-many --target=build --all --parallel=5
Distributed Computation Caching
Share cache artifacts across your entire organization, including CI agents. No one in your team ever builds or tests the same thing twice.
Distributed Task Execution
Distribute a single command across many CI machines while maintaining the developer experience of running it locally.
Detecting Affected Projects
Only build and test what’s affected by a change, rather than the entire repo.
# Only test projects affected by changes since main
npx nx affected --target=test --base=main
# Turborepo equivalent
npx turbo run test --filter=...[origin/main]
🧠 Understandable
Workspace Analysis
Understand the project graph of the workspace automatically by analyzing package.json, source files, and configuration.
Dependency Graph Visualization
Interactively visualize dependency relationships between projects and tasks.
graph TD
subgraph "Apps"
A[web-app]
B[mobile-app]
C[api-server]
end
subgraph "UI Libraries"
D[ui-button]
E[ui-form]
F[ui-modal]
end
subgraph "Shared Libraries"
G[utils-date]
H[utils-string]
I[config-eslint]
J[config-typescript]
end
A --> D
A --> E
A --> F
A --> G
A --> H
B --> D
B --> E
B --> G
B --> H
C --> G
C --> H
C --> I
C --> J
D --> G
E --> G
F --> H
style A fill:#e3f2fd
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fff3e0
style F fill:#fff3e0
style G fill:#fce4ec
style H fill:#fce4ec
style I fill:#e1f5fe
style J fill:#e1f5fe
# Nx: open interactive graph visualization
npx nx graph
# Turborepo: generate graph
npx turbo run build --graph
🔧 Manageable
Source Code Sharing
Easily share discrete pieces of source code between projects without publishing packages.
Consistent Tooling
Get a consistent experience regardless of what technology each project uses — JavaScript, TypeScript, Go, Rust, Java, etc.
Code Generation
Scaffold new projects, components, and modules with built-in generators.
# Nx: generate a new React component
npx nx g @nx/react:component my-button --project=ui-lib
# Nx: generate a new Node.js library
npx nx g @nx/node:lib shared-utils
Project Constraints and Visibility
Define rules to constrain dependency relationships. Mark projects as private, separate frontend from backend, and enforce architectural boundaries.
// nx.json - enforce module boundaries
{
"targetDefaults": {},
"plugins": [
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
}
}
]
}
Monorepo Tools Compared
Here’s how the major monorepo tools stack up:
Nx
“Nx optimizes your builds, scales your CI, and fixes failed PRs.”
- Best for: Full-featured monorepo management with excellent DX
- Language support: JavaScript/TypeScript, plus plugins for Go, Rust, Java, etc.
- Highlights: Interactive graph visualizer, powerful code generators, distributed task execution, first-class MCP server support
- Cache: Local + remote (via Nx Cloud)
Turborepo (by Vercel)
“The high-performance build system for JavaScript & TypeScript codebases.”
- Best for: Simple, fast task running in JS/TS monorepos
- Language support: JavaScript/TypeScript
- Highlights: Zero-config caching, incremental builds, remote caching via Vercel
- Cache: Local + remote
Bazel (by Google)
“A fast, scalable, multi-language and extensible build system.”
- Best for: Massive repositories (billions of lines of code), polyglot projects
- Language support: Almost any language via build rules
- Highlights: Transparent remote execution, most mature distributed execution
- Cache: Local + remote
Lerna (maintained by Nx team)
“A tool for managing JavaScript projects with multiple packages.”
- Best for: npm package publishing from monorepos
- Language support: JavaScript/TypeScript
- Highlights: Versioning and publishing workflows, powered by Nx under the hood since v6
- Cache: Local + remote (via Nx Cloud)
moon (by moonrepo)
“A task runner and monorepo management tool for the web ecosystem.”
- Best for: Web projects wanting an integrated toolchain
- Language support: JavaScript/TypeScript, plus tier 2 language support
- Highlights: Built-in toolchain management, native code generation, project constraints
Pants (by Pants Build)
“A fast, scalable, user-friendly build system for codebases of all sizes.”
- Best for: Polyglot repos, especially Python + backend languages
- Language support: Python, Java, Scala, Go, Shell, Docker
- Highlights: Automatic dependency inference via static analysis, transparent remote execution
Rush (by Microsoft)
“Geared for large monorepos with lots of teams and projects.”
- Best for: Large enterprise JavaScript/TypeScript monorepos
- Language support: JavaScript/TypeScript
- Highlights: NPM dependency approvals, version policies, BuildXL integration
Feature Comparison Matrix
| Feature | Nx | Turborepo | Bazel | Lerna | moon | Pants | Rush |
|---|---|---|---|---|---|---|---|
| Local caching | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Task orchestration | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Remote caching | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Distributed execution | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | 🔧 |
| Affected detection | ✅ | ✅ | 🔧 | ✅ | ✅ | ✅ | ✅ |
| Graph visualization | ✅ | ✅ | ✅ | ✅ | ✅ | 🔧 | 🔧 |
| Consistent tooling | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ |
| Code generation | ✅ | 🔧 | 🔧 | 🔧 | ✅ | ✅ | 🔧 |
| Project constraints | ✅ | 🔧 | ✅ | 🔧 | ✅ | 🔧 | ✅ |
✅ = Natively supported | 🔧 = Implement your own | ❌ = Not supported
Choosing the Right Tool
Here’s a quick decision guide:
- Small-to-medium JS/TS project, want simplicity → Turborepo
- Full-featured monorepo with great DX → Nx
- Massive polyglot codebase (Google-scale) → Bazel
- Need to publish npm packages → Lerna (with Nx)
- Web ecosystem with integrated tooling → moon
- Python/backend-heavy polyglot repo → Pants
- Large enterprise JS/TS with strict governance → Rush
Getting Started: Monorepo with Nx
Here’s how to set up a monorepo with Nx:
# Create a new Nx workspace
npx create-nx-workspace@latest my-monorepo
# Choose your preset (React, Angular, Node, etc.)
# or start with an empty workspace
cd my-monorepo
Adding Projects
# Add a React application
npx nx g @nx/react:app web-app
# Add a Node.js API
npx nx g @nx/node:app api
# Add a shared library
npx nx g @nx/react:lib ui-components
Running Tasks
# Build a specific project
npx nx build web-app
# Run tests for affected projects
npx nx affected --target=test
# Visualize the dependency graph
npx nx graph
# Run multiple tasks in parallel
npx nx run-many --target=build --all --parallel=5
Nx Configuration
// nx.json
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"cache": true
},
"lint": {
"cache": true
}
},
"defaultBase": "main"
}
Getting Started: Monorepo with Turborepo
# Create a new Turborepo
npx create-turbo@latest my-turborepo
cd my-turborepo
Project Structure
my-turborepo/
├── apps/
│ ├── web/ # Next.js app
│ └── docs/ # Documentation site
├── packages/
│ ├── ui/ # Shared UI components
│ ├── eslint-config/ # Shared ESLint config
│ └── typescript-config/ # Shared TS config
├── turbo.json
└── package.json
Turborepo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
Running Tasks
# Build all projects
npx turbo run build
# Run dev servers
npx turbo run dev
# Only run tasks for specific packages
npx turbo run build --filter=web
# Run affected tasks
npx turbo run test --filter=...[origin/main]
Best Practices
1. Define Clear Project Boundaries
Organize your monorepo with clear boundaries between apps and libraries:
monorepo/
├── apps/ # Deployable applications
├── packages/ # Shared libraries
├── tools/ # Build tools and scripts
└── configs/ # Shared configurations
2. Use Module Boundaries
Enforce that backend projects don’t import from frontend projects and vice versa. Tools like Nx provide built-in lint rules for this.
// .eslintrc.json with Nx module boundary rules
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "scope:frontend", "onlyDependOnLibsWithTags": ["scope:shared"] },
{ "sourceTag": "scope:backend", "onlyDependOnLibsWithTags": ["scope:shared"] }
]
}
]
}
}
3. Cache Aggressively
Enable both local and remote caching. This can reduce CI times by 50-90%.
4. Use Affected Commands in CI
Never build and test everything — only what’s affected by the current change.
# GitHub Actions example
- name: Test affected
run: npx nx affected --target=test --base=origin/main
5. Keep Shared Libraries Small and Focused
Don’t create a single “shared” library with everything. Instead, create small, focused libraries:
packages/
├── ui-button/
├── ui-form/
├── utils-date/
├── utils-string/
└── config-eslint/
6. Automate Dependency Updates
Use tools like Renovate or Dependabot to keep dependencies in sync across the entire monorepo.
7. Standardize Development Commands
Ensure every project uses the same commands for common tasks:
nx build <project>
nx test <project>
nx lint <project>
nx serve <project>
Conclusion
A monorepo is more than a code organization strategy — it’s a shift in how your organization collaborates and ships software. By consolidating projects into a single repository with well-defined relationships, you gain:
- Faster development through code sharing and caching
- Better collaboration with atomic commits and consistent tooling
- Improved code quality through enforced module boundaries
- Simplified CI/CD with affected commands and distributed execution
The tooling ecosystem has matured significantly, with options for every team size and tech stack. Whether you choose Nx for its comprehensive features, Turborepo for its simplicity, or Bazel for its scale, the key is to start small and grow your monorepo practices incrementally.
Remember: a monorepo is not a monolith. Done right, it’s the opposite — a modular, scalable, and collaborative way to manage your codebase.
Further Resources
- monorepo.tools — Comprehensive monorepo resource and tool comparison
- Nx Documentation — Full Nx monorepo documentation
- Turborepo Docs — Turborepo official documentation
- Bazel Documentation — Bazel build system docs
- Misconceptions about Monorepos: Monorepo != Monolith — Great read on monorepo myths