Monorepos consolidate multiple projects in one repository. Here's how to set up and manage them effectively.
Why Monorepos#
Benefits:
- Shared code and dependencies
- Atomic changes across packages
- Consistent tooling and config
- Simplified dependency management
Challenges:
- Build complexity
- CI performance
- Code ownership
- Repository size
Workspace Setup with pnpm#
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'project/
├── apps/
│ ├── web/
│ │ └── package.json
│ └── api/
│ └── package.json
├── packages/
│ ├── ui/
│ │ └── package.json
│ ├── utils/
│ │ └── package.json
│ └── config/
│ └── package.json
├── package.json
└── pnpm-workspace.yaml
1// Root package.json
2{
3 "name": "monorepo",
4 "private": true,
5 "scripts": {
6 "build": "turbo run build",
7 "dev": "turbo run dev",
8 "lint": "turbo run lint",
9 "test": "turbo run test"
10 },
11 "devDependencies": {
12 "turbo": "^1.10.0"
13 }
14}
15
16// apps/web/package.json
17{
18 "name": "@myapp/web",
19 "dependencies": {
20 "@myapp/ui": "workspace:*",
21 "@myapp/utils": "workspace:*"
22 }
23}
24
25// packages/ui/package.json
26{
27 "name": "@myapp/ui",
28 "main": "./dist/index.js",
29 "types": "./dist/index.d.ts",
30 "dependencies": {
31 "@myapp/utils": "workspace:*"
32 }
33}Turborepo Configuration#
1// turbo.json
2{
3 "$schema": "https://turbo.build/schema.json",
4 "globalDependencies": ["**/.env.*local"],
5 "pipeline": {
6 "build": {
7 "dependsOn": ["^build"],
8 "outputs": ["dist/**", ".next/**"]
9 },
10 "lint": {
11 "outputs": []
12 },
13 "test": {
14 "dependsOn": ["build"],
15 "outputs": []
16 },
17 "dev": {
18 "cache": false,
19 "persistent": true
20 },
21 "clean": {
22 "cache": false
23 }
24 }
25}1# Build all packages
2pnpm turbo run build
3
4# Build specific package
5pnpm turbo run build --filter=@myapp/web
6
7# Build package and dependencies
8pnpm turbo run build --filter=@myapp/web...
9
10# Build only changed packages
11pnpm turbo run build --filter=[HEAD^1]Shared Configuration#
1// packages/config/eslint.js
2module.exports = {
3 extends: [
4 'eslint:recommended',
5 'plugin:@typescript-eslint/recommended',
6 'prettier',
7 ],
8 parser: '@typescript-eslint/parser',
9 plugins: ['@typescript-eslint'],
10 rules: {
11 '@typescript-eslint/no-unused-vars': 'error',
12 },
13};
14
15// apps/web/.eslintrc.js
16module.exports = {
17 root: true,
18 extends: ['@myapp/config/eslint'],
19 parserOptions: {
20 project: './tsconfig.json',
21 },
22};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 "declarationMap": true,
12 "sourceMap": true
13 }
14}
15
16// apps/web/tsconfig.json
17{
18 "extends": "@myapp/config/tsconfig.base.json",
19 "compilerOptions": {
20 "outDir": "dist"
21 },
22 "include": ["src"]
23}Internal Packages#
1// packages/ui/src/index.ts
2export { Button } from './Button';
3export { Input } from './Input';
4export { Card } from './Card';
5export type { ButtonProps, InputProps } from './types';
6
7// packages/ui/src/Button.tsx
8import { cn } from '@myapp/utils';
9
10export interface ButtonProps {
11 children: React.ReactNode;
12 variant?: 'primary' | 'secondary';
13 onClick?: () => void;
14}
15
16export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
17 return (
18 <button
19 className={cn(
20 'px-4 py-2 rounded',
21 variant === 'primary' && 'bg-blue-500 text-white',
22 variant === 'secondary' && 'bg-gray-200 text-gray-800'
23 )}
24 onClick={onClick}
25 >
26 {children}
27 </button>
28 );
29}1// packages/ui/package.json
2{
3 "name": "@myapp/ui",
4 "version": "0.0.0",
5 "main": "./dist/index.js",
6 "module": "./dist/index.mjs",
7 "types": "./dist/index.d.ts",
8 "exports": {
9 ".": {
10 "import": "./dist/index.mjs",
11 "require": "./dist/index.js",
12 "types": "./dist/index.d.ts"
13 }
14 },
15 "scripts": {
16 "build": "tsup src/index.ts --format cjs,esm --dts",
17 "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
18 }
19}Build Caching#
1// turbo.json with remote caching
2{
3 "pipeline": {
4 "build": {
5 "outputs": ["dist/**"],
6 "cache": true
7 }
8 }
9}1# Enable remote caching
2npx turbo login
3npx turbo link
4
5# Or use environment variables
6TURBO_TOKEN=xxx
7TURBO_TEAM=myteam1# GitHub Actions with caching
2- name: Setup Turbo cache
3 uses: actions/cache@v3
4 with:
5 path: .turbo
6 key: turbo-${{ github.sha }}
7 restore-keys: |
8 turbo-
9
10- name: Build
11 run: pnpm turbo run buildCI Configuration#
1# .github/workflows/ci.yml
2name: CI
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 branches: [main]
9
10jobs:
11 build:
12 runs-on: ubuntu-latest
13 steps:
14 - uses: actions/checkout@v4
15 with:
16 fetch-depth: 2
17
18 - uses: pnpm/action-setup@v2
19 with:
20 version: 8
21
22 - uses: actions/setup-node@v4
23 with:
24 node-version: 20
25 cache: 'pnpm'
26
27 - name: Install dependencies
28 run: pnpm install
29
30 - name: Build changed packages
31 run: pnpm turbo run build --filter='[HEAD^1]'
32
33 - name: Lint
34 run: pnpm turbo run lint --filter='[HEAD^1]'
35
36 - name: Test
37 run: pnpm turbo run test --filter='[HEAD^1]'
38
39 deploy-web:
40 needs: build
41 if: github.ref == 'refs/heads/main'
42 runs-on: ubuntu-latest
43 steps:
44 - uses: actions/checkout@v4
45
46 - uses: pnpm/action-setup@v2
47 with:
48 version: 8
49
50 - run: pnpm install
51 - run: pnpm turbo run build --filter=@myapp/web...
52 - run: pnpm --filter=@myapp/web deployVersioning and Publishing#
1// Using changesets
2{
3 "devDependencies": {
4 "@changesets/cli": "^2.26.0"
5 },
6 "scripts": {
7 "changeset": "changeset",
8 "version": "changeset version",
9 "publish": "turbo run build && changeset publish"
10 }
11}1# .changeset/config.json
2{
3 "changelog": "@changesets/cli/changelog",
4 "commit": false,
5 "fixed": [],
6 "linked": [],
7 "access": "restricted",
8 "baseBranch": "main",
9 "updateInternalDependencies": "patch"
10}1# Create a changeset
2pnpm changeset
3
4# Version packages
5pnpm changeset version
6
7# Publish packages
8pnpm changeset publishTask Dependencies#
1// turbo.json
2{
3 "pipeline": {
4 "build": {
5 "dependsOn": ["^build"],
6 "outputs": ["dist/**"]
7 },
8 "deploy": {
9 "dependsOn": ["build", "test", "lint"]
10 },
11 "test": {
12 "dependsOn": ["build"]
13 },
14 "db:generate": {
15 "cache": false
16 },
17 "db:push": {
18 "cache": false,
19 "dependsOn": ["db:generate"]
20 }
21 }
22}Best Practices#
Structure:
✓ Use consistent package naming (@scope/name)
✓ Keep shared config in dedicated package
✓ Organize by feature or layer
✓ Document package dependencies
Performance:
✓ Enable build caching
✓ Use remote cache in CI
✓ Filter builds to changed packages
✓ Parallelize independent tasks
Dependencies:
✓ Use workspace protocol (workspace:*)
✓ Hoist shared dependencies
✓ Keep package versions aligned
✓ Avoid circular dependencies
CI/CD:
✓ Cache node_modules and turbo
✓ Build only affected packages
✓ Run tests in parallel
✓ Deploy packages independently
Conclusion#
Monorepos work well with proper tooling. Use pnpm workspaces for dependency management, Turborepo for build orchestration, and changesets for versioning. Focus on caching and filtering to keep builds fast as the repository grows.