What is a Monorepo
A Monorepo (Monorepo) is an architectural pattern that manages multiple projects or packages in a single repository. It’s adopted by large companies like Google, Meta, and Microsoft.
flowchart TB
subgraph Polyrepo["Polyrepo"]
RA["repo-a<br/>pkg.json"] --> npm1["npm"]
RB["repo-b<br/>pkg.json"] --> npm2["npm"]
RC["repo-c<br/>pkg.json"] --> npm3["npm"]
RD["repo-d<br/>pkg.json"] --> npm4["npm"]
end
subgraph Monorepo["Monorepo (single-repo)"]
subgraph Packages["Shared dependencies"]
UI["packages/ui"]
Utils["packages/utils"]
Web["apps/web"]
end
end
Benefits and Drawbacks of Monorepos
Benefits
| Benefit | Description |
|---|---|
| 1. Easy Code Sharing | packages/shared → apps/web (Immediately importable, no version management) |
| 2. Atomic Commits | Complete changes to multiple packages in 1 commit → Release breaking changes and fixes together |
| 3. Unified Toolchain | Centralized ESLint, TypeScript, test config |
| 4. Dependency Visualization | Clear view of inter-package dependencies |
Drawbacks and Solutions
| Drawback | Solution |
|---|---|
| Repository size growth | Sparse checkout, shallow clone |
| CI execution time increase | Differential builds, parallel execution, remote cache |
| Permission management complexity | CODEOWNERS, directory-level permissions |
| Increased conflicts | Appropriate package splitting, clear responsibility boundaries |
Directory Structure Pattern
monorepo/
├── apps/ # Applications
│ ├── web/ # Frontend
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── api/ # Backend
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── mobile/ # Mobile app
│ └── ...
├── packages/ # Shared packages
│ ├── ui/ # UI components
│ │ ├── src/
│ │ │ ├── Button/
│ │ │ ├── Modal/
│ │ │ └── index.ts
│ │ └── package.json
│ ├── utils/ # Utility functions
│ │ └── ...
│ ├── config/ # Shared configuration
│ │ ├── eslint/
│ │ ├── typescript/
│ │ └── tailwind/
│ └── types/ # Shared type definitions
│ └── ...
├── tools/ # Development tools/scripts
│ ├── scripts/
│ └── generators/
├── package.json # Root package.json
├── pnpm-workspace.yaml # Workspace configuration
├── turbo.json # Turborepo configuration
└── tsconfig.base.json # Base TypeScript configuration
pnpm Workspaces Configuration
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tools/*"
// Root package.json
{
"name": "monorepo",
"private": true,
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"clean": "turbo run clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
},
"packageManager": "pnpm@9.0.0"
}
Internal Package References
// apps/web/package.json
{
"name": "@monorepo/web",
"dependencies": {
"@monorepo/ui": "workspace:*",
"@monorepo/utils": "workspace:*",
"react": "^19.0.0"
}
}
// apps/web/src/App.tsx
import { Button, Modal } from '@monorepo/ui';
import { formatDate, debounce } from '@monorepo/utils';
export function App() {
return (
<div>
<Button onClick={() => console.log(formatDate(new Date()))}>
Click
</Button>
</div>
);
}
Build Optimization with Turborepo
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
Task Dependency Visualization
When running turbo run build:
flowchart TB
Types["@repo/types<br/>build"] --> Utils["@repo/utils<br/>build"]
Utils --> UI["@repo/ui<br/>build"]
Utils --> API["@repo/api<br/>build"]
Utils --> Web["@repo/web<br/>build"]
- ^build: Build dependent packages first
- Parallel execution: Tasks without dependencies run simultaneously
Remote Cache Configuration
# Vercel Remote Cache
npx turbo login
npx turbo link
# Self-hosted cache (ducktape)
# turbo.json
{
"remoteCache": {
"signature": true
}
}
Turborepo vs Nx Comparison
| Aspect | Turborepo | Nx |
|---|---|---|
| Learning Curve | Low | Medium-High |
| Configuration | Simple | Feature-rich |
| Generators | None | Extensive |
| Plugins | Limited | Abundant |
| Cache | Vercel integrated | Nx Cloud |
| Dependency Analysis | Basic | Detailed |
| IDE Integration | Basic | VSCode extension |
Selection criteria:
- Simplicity priority → Turborepo
- Enterprise features → Nx
- Using Vercel → Turborepo
- Using Angular → Nx
CI/CD Strategy
# .github/workflows/ci.yml
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 # For diff detection
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm turbo run build --filter="...[HEAD^1]"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test
run: pnpm turbo run test --filter="...[HEAD^1]"
- name: Lint
run: pnpm turbo run lint --filter="...[HEAD^1]"
Filtering Syntax
# Build only changed packages
turbo run build --filter="...[HEAD^1]"
# Specific package and its dependencies
turbo run build --filter="@repo/web..."
# Specific package and its dependents
turbo run build --filter="...@repo/ui"
# Only under specific directory
turbo run build --filter="./apps/*"
Best Practices
- Clear package responsibilities: One package, one responsibility
- Avoid circular dependencies: Dependency graph should always be a DAG
- Versioning strategy: Unified version management with Changesets, etc.
- Documentation: Place README in each package
- Appropriate granularity: Not too fine, not too large package splitting