A monorepo contains multiple projects in a single repository. When managed well, monorepos enable code sharing, atomic changes, and simplified dependency management. When managed poorly, they become slow and unwieldy. Here's how to get it right.
Why Monorepos?#
Benefits#
✓ Atomic changes across packages
✓ Single source of truth
✓ Simplified dependency management
✓ Easier code sharing
✓ Consistent tooling
✓ Better visibility
Challenges#
✗ Build times can grow
✗ CI complexity increases
✗ Git operations slow down
✗ Ownership can be unclear
✗ Learning curve for tools
Monorepo Structure#
Typical Layout#
my-monorepo/
├── apps/
│ ├── web/ # Next.js web app
│ ├── mobile/ # React Native app
│ └── api/ # Node.js API
├── packages/
│ ├── ui/ # Shared component library
│ ├── config/ # Shared configs (ESLint, TS)
│ ├── utils/ # Shared utilities
│ └── database/ # Database client
├── turbo.json # Turborepo config
├── package.json # Root package.json
└── pnpm-workspace.yaml # Workspace definition
Workspace Definition#
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'1// package.json (root)
2{
3 "name": "my-monorepo",
4 "private": true,
5 "workspaces": [
6 "apps/*",
7 "packages/*"
8 ],
9 "scripts": {
10 "build": "turbo run build",
11 "dev": "turbo run dev",
12 "test": "turbo run test",
13 "lint": "turbo run lint"
14 },
15 "devDependencies": {
16 "turbo": "^1.10.0"
17 }
18}Build Tools#
Turborepo#
1// turbo.json
2{
3 "$schema": "https://turbo.build/schema.json",
4 "globalDependencies": ["**/.env"],
5 "pipeline": {
6 "build": {
7 "dependsOn": ["^build"],
8 "outputs": ["dist/**", ".next/**"]
9 },
10 "test": {
11 "dependsOn": ["build"],
12 "outputs": []
13 },
14 "lint": {
15 "outputs": []
16 },
17 "dev": {
18 "cache": false,
19 "persistent": true
20 }
21 }
22}Key features:
- Task caching (local and remote)
- Parallel execution
- Dependency graph awareness
- Incremental builds
Nx#
1// nx.json
2{
3 "targetDefaults": {
4 "build": {
5 "dependsOn": ["^build"],
6 "cache": true
7 },
8 "test": {
9 "cache": true
10 }
11 },
12 "affected": {
13 "defaultBase": "main"
14 }
15}Key features:
- Computation caching
- Distributed task execution
- Project graph visualization
- Code generators
Package Management#
Internal Dependencies#
1// apps/web/package.json
2{
3 "name": "@myorg/web",
4 "dependencies": {
5 "@myorg/ui": "workspace:*",
6 "@myorg/utils": "workspace:*"
7 }
8}Shared Configurations#
1// packages/config/eslint-config.js
2module.exports = {
3 extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4 rules: {
5 // Shared rules
6 },
7};
8
9// apps/web/.eslintrc.js
10module.exports = {
11 extends: ['@myorg/eslint-config'],
12 rules: {
13 // App-specific overrides
14 },
15};TypeScript Configuration#
1// packages/config/tsconfig.base.json
2{
3 "compilerOptions": {
4 "target": "ES2020",
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "strict": true,
8 "esModuleInterop": true,
9 "skipLibCheck": true,
10 "declaration": true
11 }
12}
13
14// apps/web/tsconfig.json
15{
16 "extends": "@myorg/config/tsconfig.base.json",
17 "compilerOptions": {
18 "outDir": "./dist"
19 },
20 "include": ["src"]
21}Task Orchestration#
Running Commands#
1# Run in all packages
2pnpm --filter '*' build
3
4# Run in specific package
5pnpm --filter @myorg/web dev
6
7# Run in package and dependencies
8pnpm --filter @myorg/web... build
9
10# Run in changed packages
11pnpm --filter '...[HEAD~1]' testWith Turborepo#
1# Run build with caching
2turbo run build
3
4# Run for specific app
5turbo run build --filter=@myorg/web
6
7# Run affected by changes
8turbo run test --filter='[HEAD~1]'
9
10# Parallel dev servers
11turbo run devCaching#
Local Caching#
1# Turborepo caches in node_modules/.cache/turbo
2# Second run is instant if inputs unchanged
3
4$ turbo run build
5• Packages in scope: @myorg/ui, @myorg/web
6• Running build in 2 packages
7@myorg/ui:build: cache miss, executing
8@myorg/web:build: cache miss, executing
9
10$ turbo run build
11• Running build in 2 packages
12@myorg/ui:build: cache hit, replaying output
13@myorg/web:build: cache hit, replaying outputRemote Caching#
1# Turborepo Remote Cache
2npx turbo login
3npx turbo link
4
5# Or self-hosted
6turbo run build --api="https://cache.mycompany.com" --token="xxx"CI/CD Optimization#
Affected-Only Builds#
1# .github/workflows/ci.yml
2name: CI
3
4on:
5 pull_request:
6
7jobs:
8 build:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12 with:
13 fetch-depth: 0
14
15 - uses: pnpm/action-setup@v2
16
17 - uses: actions/cache@v3
18 with:
19 path: node_modules/.cache/turbo
20 key: turbo-${{ runner.os }}-${{ github.sha }}
21 restore-keys: turbo-${{ runner.os }}-
22
23 - run: pnpm install
24
25 # Only test affected packages
26 - run: pnpm turbo run test --filter='[origin/main...HEAD]'Parallel Jobs#
1jobs:
2 build:
3 strategy:
4 matrix:
5 package: [web, api, mobile]
6 steps:
7 - run: pnpm turbo run build --filter=@myorg/${{ matrix.package }}Code Sharing Patterns#
Shared Components#
1// packages/ui/src/Button.tsx
2export interface ButtonProps {
3 variant: 'primary' | 'secondary';
4 children: React.ReactNode;
5 onClick?: () => void;
6}
7
8export function Button({ variant, children, onClick }: ButtonProps) {
9 return (
10 <button className={`btn btn-${variant}`} onClick={onClick}>
11 {children}
12 </button>
13 );
14}
15
16// packages/ui/src/index.ts
17export * from './Button';
18export * from './Input';
19export * from './Card';1// apps/web/src/pages/index.tsx
2import { Button } from '@myorg/ui';
3
4export default function Home() {
5 return <Button variant="primary">Click me</Button>;
6}Shared Types#
1// packages/types/src/user.ts
2export interface User {
3 id: string;
4 email: string;
5 name: string;
6}
7
8// apps/api/src/routes/users.ts
9import type { User } from '@myorg/types';
10
11// apps/web/src/hooks/useUser.ts
12import type { User } from '@myorg/types';Versioning and Publishing#
Changesets#
1# Install changesets
2pnpm add -D @changesets/cli
3
4# Initialize
5pnpm changeset init
6
7# Create changeset
8pnpm changeset
9# Interactive prompts for version bumps
10
11# Version packages
12pnpm changeset version
13
14# Publish
15pnpm changeset publish1# .changeset/config.json
2{
3 "$schema": "https://unpkg.com/@changesets/config/schema.json",
4 "changelog": "@changesets/cli/changelog",
5 "commit": false,
6 "access": "restricted",
7 "baseBranch": "main"
8}Best Practices#
Ownership#
# CODEOWNERS
/apps/web/ @frontend-team
/apps/api/ @backend-team
/packages/ui/ @design-system-team
/packages/database/ @platform-team
Consistent Scripts#
1// Every package has the same scripts
2{
3 "scripts": {
4 "build": "...",
5 "dev": "...",
6 "test": "...",
7 "lint": "...",
8 "typecheck": "..."
9 }
10}Dependency Hygiene#
1# Find unused dependencies
2pnpm dlx depcheck
3
4# Update all dependencies
5pnpm update -r
6
7# Check for mismatched versions
8pnpm dedupe --checkDocumentation#
1# CONTRIBUTING.md
2
3## Adding a New Package
4
51. Create directory in `packages/` or `apps/`
62. Add `package.json` with standard scripts
73. Add to `pnpm-workspace.yaml` if using different pattern
84. Run `pnpm install`
9
10## Development
11
12- `pnpm dev` - Start all dev servers
13- `pnpm build` - Build all packages
14- `pnpm test` - Run all tests
15
16## Creating a Changeset
17
181. Run `pnpm changeset`
192. Select affected packages
203. Describe changes
214. Commit the changeset fileConclusion#
Monorepos shine when you need tight coordination between packages, shared tooling, and atomic changes. Modern tools like Turborepo and Nx make them practical even at scale.
Start with a clear structure, invest in shared configurations, and leverage caching aggressively. The upfront investment pays dividends in developer productivity and code quality.
The key is treating the monorepo as a product—it needs maintenance, documentation, and continuous improvement to serve your teams well.