Monorepos keep all your code in one repository. Done right, they improve code sharing, simplify refactoring, and streamline CI/CD. Here's how to implement them effectively.
Why Monorepo?#
Benefits:
✓ Shared code without publishing
✓ Atomic changes across packages
✓ Unified tooling and config
✓ Easier refactoring
✓ Single source of truth
✓ Simplified dependency management
Challenges:
✗ Longer initial clone
✗ Complex build orchestration
✗ Requires good tooling
✗ CI/CD complexity
Project Structure#
my-monorepo/
├── apps/
│ ├── web/ # Next.js frontend
│ │ ├── src/
│ │ └── package.json
│ ├── api/ # Express backend
│ │ ├── src/
│ │ └── package.json
│ └── mobile/ # React Native
│ ├── src/
│ └── package.json
├── packages/
│ ├── ui/ # Shared UI components
│ │ ├── src/
│ │ └── package.json
│ ├── config/ # Shared configs
│ │ ├── eslint/
│ │ ├── typescript/
│ │ └── package.json
│ └── utils/ # Shared utilities
│ ├── src/
│ └── package.json
├── package.json # Root package.json
├── pnpm-workspace.yaml # Workspace config
└── turbo.json # Turborepo config
Workspace Setup (pnpm)#
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'1// Root package.json
2{
3 "name": "my-monorepo",
4 "private": true,
5 "scripts": {
6 "dev": "turbo dev",
7 "build": "turbo build",
8 "test": "turbo test",
9 "lint": "turbo lint",
10 "clean": "turbo clean"
11 },
12 "devDependencies": {
13 "turbo": "^2.0.0"
14 }
15}1// apps/web/package.json
2{
3 "name": "@myorg/web",
4 "private": true,
5 "dependencies": {
6 "@myorg/ui": "workspace:*",
7 "@myorg/utils": "workspace:*",
8 "next": "^14.0.0"
9 }
10}Turborepo Configuration#
1// turbo.json
2{
3 "$schema": "https://turbo.build/schema.json",
4 "globalDependencies": ["**/.env.*local"],
5 "tasks": {
6 "build": {
7 "dependsOn": ["^build"],
8 "outputs": [".next/**", "dist/**"]
9 },
10 "dev": {
11 "cache": false,
12 "persistent": true
13 },
14 "test": {
15 "dependsOn": ["build"],
16 "inputs": ["src/**/*.ts", "test/**/*.ts"]
17 },
18 "lint": {
19 "dependsOn": ["^build"],
20 "outputs": []
21 },
22 "clean": {
23 "cache": false
24 }
25 }
26}Shared Configurations#
1// packages/config/eslint/index.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 // Shared rules
12 },
13};
14
15// Usage in apps/web/.eslintrc.js
16module.exports = {
17 extends: ['@myorg/config/eslint'],
18 // App-specific overrides
19};1// packages/config/typescript/base.json
2{
3 "$schema": "https://json.schemastore.org/tsconfig",
4 "compilerOptions": {
5 "target": "ES2020",
6 "module": "ESNext",
7 "moduleResolution": "bundler",
8 "strict": true,
9 "skipLibCheck": true,
10 "declaration": true,
11 "declarationMap": true,
12 "sourceMap": true
13 }
14}
15
16// apps/web/tsconfig.json
17{
18 "extends": "@myorg/config/typescript/nextjs.json",
19 "compilerOptions": {
20 "baseUrl": ".",
21 "paths": {
22 "@/*": ["./src/*"]
23 }
24 },
25 "include": ["src"],
26 "exclude": ["node_modules"]
27}Shared UI Package#
1// packages/ui/src/button.tsx
2import { forwardRef, ButtonHTMLAttributes } from 'react';
3import { cn } from './utils';
4
5interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
6 variant?: 'primary' | 'secondary' | 'outline';
7 size?: 'sm' | 'md' | 'lg';
8}
9
10export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
11 ({ className, variant = 'primary', size = 'md', ...props }, ref) => {
12 return (
13 <button
14 ref={ref}
15 className={cn(
16 'rounded font-medium transition-colors',
17 variants[variant],
18 sizes[size],
19 className
20 )}
21 {...props}
22 />
23 );
24 }
25);
26
27Button.displayName = 'Button';1// packages/ui/package.json
2{
3 "name": "@myorg/ui",
4 "version": "0.0.0",
5 "main": "./src/index.ts",
6 "types": "./src/index.ts",
7 "exports": {
8 ".": "./src/index.ts",
9 "./button": "./src/button.tsx",
10 "./input": "./src/input.tsx"
11 },
12 "peerDependencies": {
13 "react": "^18.0.0"
14 }
15}Version Management#
1# Using Changesets for versioning
2npx changeset init
3
4# Create a changeset
5npx changeset
6# Select packages that changed
7# Write changelog entry
8
9# Version packages
10npx changeset version
11
12# Publish packages
13npx changeset publish1// .changeset/config.json
2{
3 "$schema": "https://unpkg.com/@changesets/config/schema.json",
4 "changelog": "@changesets/cli/changelog",
5 "commit": false,
6 "fixed": [],
7 "linked": [],
8 "access": "restricted",
9 "baseBranch": "main",
10 "updateInternalDependencies": "patch"
11}CI/CD Pipeline#
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
14 steps:
15 - uses: actions/checkout@v4
16 with:
17 fetch-depth: 2
18
19 - uses: pnpm/action-setup@v3
20 with:
21 version: 8
22
23 - uses: actions/setup-node@v4
24 with:
25 node-version: 20
26 cache: 'pnpm'
27
28 - name: Install dependencies
29 run: pnpm install --frozen-lockfile
30
31 - name: Build
32 run: pnpm build
33
34 - name: Test
35 run: pnpm test
36
37 - name: Lint
38 run: pnpm lint
39
40 # Only run affected packages
41 affected:
42 runs-on: ubuntu-latest
43 steps:
44 - uses: actions/checkout@v4
45 with:
46 fetch-depth: 0
47
48 - uses: pnpm/action-setup@v3
49 - uses: actions/setup-node@v4
50
51 - run: pnpm install --frozen-lockfile
52
53 # Only test changed packages
54 - name: Test affected
55 run: pnpm turbo test --filter='...[origin/main]'Remote Caching#
# Turborepo remote caching with Vercel
npx turbo login
npx turbo link1// turbo.json with remote caching
2{
3 "remoteCache": {
4 "signature": true
5 }
6}Best Practices#
DO:
✓ Use consistent tooling across packages
✓ Share configurations as packages
✓ Implement proper dependency boundaries
✓ Use remote caching for CI
✓ Only build/test affected packages
✓ Document package relationships
DON'T:
✗ Create circular dependencies
✗ Share too much code (tight coupling)
✗ Skip proper versioning
✗ Ignore build performance
✗ Put everything in one package
Task Orchestration#
1# Run specific package
2pnpm --filter @myorg/web dev
3
4# Run package and dependencies
5pnpm --filter @myorg/web... build
6
7# Run dependents of a package
8pnpm --filter ...@myorg/ui build
9
10# Run affected since main
11pnpm turbo build --filter='...[origin/main]'Conclusion#
Monorepos enable code sharing and atomic changes at scale. Use proper tooling (Turborepo, pnpm), implement remote caching, and maintain clear package boundaries.
Start simple, add complexity as needed, and invest in build performance early.