As codebases grow and teams scale, managing multiple related projects becomes increasingly complex. Monorepos—storing multiple projects in a single repository—offer solutions for code sharing, atomic changes, and unified tooling. This guide covers when to use monorepos, which tools to choose, and how to optimize build performance at scale.
Monorepo vs Polyrepo
┌─────────────────────────────────────────────────────────────┐
│ POLYREPO STRUCTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ repo-web │ │ repo-api │ │ repo-shared │ │
│ │ ├── src/ │ │ ├── src/ │ │ ├── src/ │ │
│ │ ├── tests/ │ │ ├── tests/ │ │ ├── tests/ │ │
│ │ └── ... │ │ └── ... │ │ └── ... │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ Published to npm registry │
│ │
├─────────────────────────────────────────────────────────────┤
│ MONOREPO STRUCTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ single-repo │ │
│ │ ├── apps/ │ │
│ │ │ ├── web/ │ │
│ │ │ └── api/ │ │
│ │ ├── packages/ │ │
│ │ │ ├── shared-ui/ │ │
│ │ │ ├── shared-utils/ │ │
│ │ │ └── config/ │ │
│ │ └── package.json (workspaces) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Internal imports via workspace protocol │
│ │
└─────────────────────────────────────────────────────────────┘
Decision Matrix
| Factor | Monorepo Favored | Polyrepo Favored |
|---|---|---|
| Code sharing | Frequent sharing across projects | Minimal shared code |
| Team structure | Cross-functional teams | Autonomous teams |
| Release cadence | Coordinated releases | Independent releases |
| Dependencies | Shared dependency versions | Different dep versions |
| Tooling | Unified build/test/lint | Project-specific tooling |
| Repo size | Manageable (<10GB) | Very large binaries |
| Open source | Internal/enterprise | Public packages |
Monorepo Tools Comparison
┌─────────────────────────────────────────────────────────────┐
│ MONOREPO TOOL LANDSCAPE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Full-Featured Frameworks │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Nx │ Complete monorepo solution │ │
│ │ │ Generators, graph, plugins, cloud │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ Bazel │ Google's build system │ │
│ │ │ Massive scale, complex setup │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Build Focused │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Turborepo │ Fast builds, simple config │ │
│ │ │ Remote caching, task pipelines │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Package Management │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Lerna │ npm publishing workflows │ │
│ │ Rush │ Microsoft's enterprise solution │ │
│ │ pnpm │ Workspace-native package manager │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Tool Selection Guide
| Tool | Best For | Learning Curve | Remote Cache |
|---|---|---|---|
| Nx | Large teams, framework-heavy projects | Medium | Nx Cloud (free tier) |
| Turborepo | Simple setups, Vercel users | Low | Vercel (free tier) |
| Bazel | Massive scale (1000+ engineers) | High | Self-hosted |
| Lerna | npm package publishing | Low | Via Nx |
| pnpm | Workspace management only | Low | None built-in |
Setting Up Turborepo
Turborepo focuses on build optimization with minimal configuration:
# Create new monorepo
npx create-turbo@latest my-monorepo
# Or add to existing repo
npm install turbo --save-dev
Project structure:
my-monorepo/
├── apps/
│ ├── web/
│ │ └── package.json
│ └── api/
│ └── package.json
├── packages/
│ ├── ui/
│ │ └── package.json
│ └── config/
│ └── package.json
├── package.json
└── turbo.json
Root package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"devDependencies": {
"turbo": "^2.0.0"
},
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test"
}
}
turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Turborepo Remote Caching
# Login to Vercel
npx turbo login
# Link repository
npx turbo link
# Now builds are cached remotely
npx turbo build
# First run: builds everything
# Second run: restores from cache in seconds
Setting Up Nx
Nx provides a complete monorepo solution with code generation and plugins:
# Create new Nx workspace
npx create-nx-workspace@latest my-workspace
# Or add Nx to existing repo
npx nx@latest init
Nx workspace structure:
my-workspace/
├── apps/
│ ├── web/
│ │ ├── src/
│ │ └── project.json
│ └── api/
│ ├── src/
│ └── project.json
├── libs/
│ ├── shared-ui/
│ │ └── project.json
│ └── shared-utils/
│ └── project.json
├── nx.json
└── package.json
nx.json:
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"lint": {
"cache": true
},
"test": {
"cache": true
}
},
"defaultBase": "main"
}
project.json example:
{
"name": "web",
"sourceRoot": "apps/web/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/next:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/web"
}
},
"serve": {
"executor": "@nx/next:server",
"options": {
"buildTarget": "web:build"
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/web/jest.config.ts"
}
}
}
}
Nx Commands
# Run task for specific project
nx build web
nx test api
# Run affected commands (only changed projects)
nx affected -t build
nx affected -t test
# View dependency graph
nx graph
# Generate new projects
nx g @nx/react:app my-new-app
nx g @nx/react:lib my-new-lib
# Enable remote caching
nx connect
Task Pipelines and Caching
Understanding Task Dependencies
┌─────────────────────────────────────────────────────────────┐
│ TASK PIPELINE │
├─────────────────────────────────────────────────────────────┤
│ │
│ "dependsOn": ["^build"] │
│ │
│ ^ = dependencies of this package │
│ │
│ Example: Building 'web' app │
│ │
│ ┌─────────────────┐ │
│ │ shared-ui │ ── build first │
│ │ (dependency) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ shared-utils │ ── build second │
│ │ (dependency) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ web │ ── build last │
│ │ (application) │ │
│ └─────────────────┘ │
│ │
│ Parallel execution where possible │
│ │
└─────────────────────────────────────────────────────────────┘
Cache Configuration
Turborepo cache keys:
{
"tasks": {
"build": {
"outputs": ["dist/**"],
"inputs": [
"src/**",
"!src/**/*.test.ts"
]
}
},
"globalEnv": ["NODE_ENV", "CI"],
"globalDependencies": [".env"]
}
Nx cache configuration:
{
"targetDefaults": {
"build": {
"inputs": [
"default",
"^default",
"{projectRoot}/**/*.ts",
"!{projectRoot}/**/*.spec.ts"
],
"outputs": ["{projectRoot}/dist"]
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"production": [
"default",
"!{projectRoot}/**/*.spec.ts",
"!{projectRoot}/jest.config.ts"
]
}
}
Affected Commands
Run tasks only on changed projects and their dependents:
# Turborepo
npx turbo build --filter=...[origin/main]
# Nx
nx affected -t build
nx affected -t test
nx affected -t lint
# Specify comparison base
nx affected -t build --base=main --head=HEAD
How Affected Analysis Works
┌─────────────────────────────────────────────────────────────┐
│ AFFECTED ANALYSIS │
├─────────────────────────────────────────────────────────────┤
│ │
│ Changed file: packages/shared-utils/src/format.ts │
│ │
│ Dependency Graph: │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ web │───▶│ shared-ui │───▶│shared-utils│ ◀── │
│ └────────────┘ └────────────┘ └────────────┘ │ │
│ │ │ │
│ │ ┌────────────┐ │ │
│ └──────────▶│ config │ │ │
│ └────────────┘ │ │
│ │ │
│ ┌────────────┐ ┌────────────┐ │ │
│ │ api │───▶│shared-utils│ ◀─────────────────┘ │
│ └────────────┘ └──────┬─────┘ │
│ │ CHANGED │
│ │
│ Affected: shared-utils, shared-ui, web, api │
│ Not affected: config (no dependency on shared-utils) │
│ │
└─────────────────────────────────────────────────────────────┘
CI/CD for Monorepos
GitHub Actions with Turborepo
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Needed for affected detection
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- name: Build
run: pnpm turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test
run: pnpm turbo test
GitHub Actions with Nx
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for affected detection
- uses: nrwl/nx-set-shas@v4
with:
main-branch-name: main
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- name: Run affected commands
run: |
npx nx affected -t lint --parallel=3
npx nx affected -t test --parallel=3 --configuration=ci
npx nx affected -t build --parallel=3
Distributed Task Execution
For large monorepos, distribute work across multiple CI agents:
Nx Cloud distributed execution:
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
- name: Start Nx Agents
run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"
- name: Run Tasks
run: npx nx affected -t lint test build --parallel=3
Versioning and Publishing
Changesets for Version Management
# Install changesets
npm install @changesets/cli --save-dev
npx changeset init
# When making changes, add a changeset
npx changeset
# Interactive: select packages, bump type, description
# In CI: version packages
npx changeset version
# In CI: publish to npm
npx changeset publish
Changeset file (.changeset/happy-dogs-jump.md):
---
"@myorg/shared-utils": minor
"@myorg/web": patch
---
Added new date formatting utilities to shared-utils.
Updated web app to use new formatters.
Lerna Publishing
# Initialize Lerna
npx lerna init
# Publish changed packages
npx lerna publish
# Publish with specific version bump
npx lerna publish patch
# Publish from CI
npx lerna publish from-package --yes
Performance Optimization
Optimizing Build Performance
| Technique | Tool Support | Impact |
|---|---|---|
| Local caching | All | 50-80% faster repeat builds |
| Remote caching | Turbo, Nx, Bazel | 90%+ faster CI |
| Affected commands | Turbo, Nx, Bazel | Skip unchanged projects |
| Parallel execution | All | Utilize all CPU cores |
| Distributed execution | Nx Cloud, Bazel | Scale across machines |
| Incremental builds | All | Only rebuild changed files |
Debugging Cache Misses
# Turborepo: see why cache missed
TURBO_LOG_VERBOSITY=1 npx turbo build
# Nx: show cache hash details
NX_VERBOSE_LOGGING=true npx nx build web
# Common causes:
# - Environment variable differences
# - Different dependency versions
# - Changed global files (package-lock.json)
# - Timestamps in outputs
Monorepo Best Practices
Directory Structure
monorepo/
├── apps/ # Deployable applications
│ ├── web/
│ ├── mobile/
│ └── api/
├── packages/ # Shared libraries
│ ├── ui/ # Component library
│ ├── utils/ # Utility functions
│ ├── config/ # Shared configs
│ └── types/ # Shared TypeScript types
├── tools/ # Build tools, scripts
├── .github/ # CI/CD workflows
├── package.json # Root package, workspaces
├── turbo.json # Turborepo config
└── nx.json # Nx config (if using Nx)
Internal Dependencies
Use workspace protocol for internal packages:
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:^1.0.0"
}
}
| Protocol | Behavior |
|---|---|
workspace:* | Always use local version |
workspace:^1.0.0 | Use local if compatible |
workspace:~1.0.0 | Use local if compatible (stricter) |
Code Sharing Guidelines
┌─────────────────────────────────────────────────────────────┐
│ CODE SHARING DECISION TREE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Is this code used by multiple projects? │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ NO YES │
│ │ │ │
│ ▼ ▼ │
│ Keep in app Is it stable? │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ NO YES │
│ │ │ │
│ ▼ ▼ │
│ Keep in app until stable Create shared package │
│ │ │
│ ┌─────────────────┴───────┐ │
│ │ │ │
│ Internal External │
│ │ │ │
│ ▼ ▼ │
│ packages/lib Publish to npm │
│ │
└─────────────────────────────────────────────────────────────┘
Related Resources
- Git & GitHub Complete Guide - Hub for all Git guides
- GitHub Actions CI/CD - Workflow automation
- Git Branching Strategies - Branch management
- Git for Large Files - Managing binary assets