Monorepo Engineering: Managing Code at Scale

Master Monorepo architecture and practical techniques with pnpm workspace, Turborepo, and more

Monorepo Engineering: Managing Code at Scale

As projects grow, code organization becomes critical. Monorepo (single repository) allows multiple related projects to coexist in one repository with unified management and code sharing. This article will help you master Monorepo engineering practices.

Why Choose Monorepo?

Polyrepo vs Monorepo

Polyrepo (Multiple Repos):
┌─────────────────────────────────────────────────────┐
│  repo-web/        repo-api/        repo-shared/    │
│  ┌─────────┐      ┌─────────┐      ┌─────────┐     │
│  │ package │      │ package │      │ package │     │
│  │ .json   │      │ .json   │      │ .json   │     │
│  └─────────┘      └─────────┘      └─────────┘     │
│       ↓                ↓                ↓          │
│  Separate         Separate          Separate       │
│  versions         versions          versions       │
│  CI/CD            CI/CD             CI/CD          │
│                                                     │
│  Problems:                                          │
│  • Cross-repo changes difficult (multiple PRs)      │
│  • Dependency versions hard to sync                 │
│  • Code reuse requires publishing npm packages      │
└─────────────────────────────────────────────────────┘

Monorepo (Single Repo):
┌─────────────────────────────────────────────────────┐
│  monorepo/                                          │
│  ├── packages/                                      │
│  │   ├── web/                                       │
│  │   ├── api/                                       │
│  │   └── shared/                                    │
│  ├── package.json                                   │
│  └── pnpm-workspace.yaml                            │
│                                                     │
│  Advantages:                                        │
│  • Atomic commits (one PR across packages)          │
│  • Unified dependency management                    │
│  • Instant code sharing (no publishing)             │
│  • Unified toolchain configuration                  │
└─────────────────────────────────────────────────────┘

When to Use

ScenarioRecommendedReason
Multiple closely related apps✅ MonorepoFrequent code sharing
Component lib + docs + examples✅ MonorepoNeed synchronized updates
Independent unrelated projects❌ PolyrepoNo sharing needs
Small team / fast iteration✅ MonorepoSimplifies collaboration
Very large org (10k+ people)⚠️ DependsNeeds more complex tools

Project Structure Design

my-monorepo/
├── apps/                    # Applications
│   ├── web/                 # Main website
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   ├── admin/               # Admin dashboard
│   │   ├── src/
│   │   └── package.json
│   └── api/                 # API service
│       ├── src/
│       └── package.json

├── packages/                # Shared packages
│   ├── ui/                  # UI component library
│   │   ├── src/
│   │   │   ├── Button/
│   │   │   ├── Input/
│   │   │   └── index.ts
│   │   └── package.json
│   ├── utils/               # Utility functions
│   │   ├── src/
│   │   └── package.json
│   ├── config/              # Shared configs
│   │   ├── eslint/
│   │   ├── tsconfig/
│   │   └── package.json
│   └── types/               # Type definitions
│       ├── src/
│       └── package.json

├── tools/                   # Development tools
│   └── scripts/

├── package.json             # Root config
├── pnpm-workspace.yaml      # Workspace config
├── turbo.json               # Turborepo config
└── tsconfig.json            # Root TS config

Package Naming Conventions

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./button": {
      "import": "./dist/button.mjs",
      "require": "./dist/button.js"
    }
  }
}

// apps/web/package.json
{
  "name": "@myorg/web",
  "private": true,
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

pnpm Workspace

pnpm is the best package manager choice for Monorepo.

Basic Configuration

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'
// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.3.0"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  },
  "packageManager": "pnpm@8.15.0"
}

Common Commands

# Install all dependencies
pnpm install

# Add dependency to specific package
pnpm add lodash --filter @myorg/utils

# Add dev dependency to root
pnpm add -Dw typescript

# Add dependency to all packages
pnpm add -r dayjs

# Run script in specific package
pnpm --filter @myorg/web dev
pnpm --filter "./apps/*" build

# Run script in package and its dependencies
pnpm --filter @myorg/web... build

# Run script in all packages that depend on a package
pnpm --filter ...@myorg/ui build

Internal Dependencies

// apps/web/package.json
{
  "dependencies": {
    // workspace:* uses latest workspace version
    "@myorg/ui": "workspace:*",
    // workspace:^ converts to actual version on publish
    "@myorg/utils": "workspace:^"
  }
}

Turborepo: Build Acceleration

Turborepo dramatically improves build speed through smart caching and task orchestration.

Basic Configuration

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    ".env",
    "tsconfig.json"
  ],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "package.json", "tsconfig.json"],
      "outputs": ["dist/**", ".next/**"],
      "cache": true
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "test/**"],
      "cache": true
    },
    "clean": {
      "cache": false
    }
  }
}

Task Dependency Graph

dependsOn Configuration Explained:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   "^build"  - Build all dependencies first          │
│                                                     │
│   @myorg/ui ─────┐                                  │
│                  ├──→ @myorg/web (build)            │
│   @myorg/utils ──┘                                  │
│                                                     │
│   "build"   - Complete own build first              │
│                                                     │
│   @myorg/web (build) ──→ @myorg/web (test)          │
│                                                     │
└─────────────────────────────────────────────────────┘

Remote Caching

# Login to Vercel (Turborepo official cache)
npx turbo login

# Link to remote cache
npx turbo link

# Or self-host
# turbo.json
{
  "remoteCache": {
    "signature": true
  }
}

# Set environment variables
TURBO_API=https://your-cache-server.com
TURBO_TOKEN=your-token
TURBO_TEAM=your-team

Filtered Execution

# Build specific package only
turbo run build --filter=@myorg/web

# Build package and its dependencies
turbo run build --filter=@myorg/web...

# Build all packages that depend on a package
turbo run build --filter=...@myorg/ui

# Build only changed packages
turbo run build --filter=[origin/main]

# Combined usage
turbo run build --filter=@myorg/web...[origin/main]

Shared Configuration

TypeScript Configuration

// packages/config/tsconfig/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true
  }
}

// packages/config/tsconfig/react.json
{
  "extends": "./base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["DOM", "DOM.Iterable", "ES2022"]
  }
}

// packages/config/tsconfig/node.json
{
  "extends": "./base.json",
  "compilerOptions": {
    "module": "CommonJS",
    "lib": ["ES2022"]
  }
}
// apps/web/tsconfig.json
{
  "extends": "@myorg/config/tsconfig/react.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

ESLint Configuration

// packages/config/eslint/base.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/explicit-function-return-type': 'off'
  }
};

// packages/config/eslint/react.js
module.exports = {
  extends: [
    './base.js',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended'
  ],
  settings: {
    react: { version: 'detect' }
  },
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off'
  }
};
// apps/web/.eslintrc.js
module.exports = {
  root: true,
  extends: ['@myorg/config/eslint/react']
};

Version Management & Publishing

Changesets

Changesets is the standard tool for Monorepo version management.

# Install
pnpm add -Dw @changesets/cli

# Initialize
pnpm changeset init
// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [["@myorg/ui", "@myorg/utils"]],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@myorg/web", "@myorg/admin"]
}

Release Workflow

# 1. Add changeset
pnpm changeset
# Select changed packages
# Choose version type (patch/minor/major)
# Write change description

# 2. Version packages
pnpm changeset version
# Auto-updates package.json and CHANGELOG.md

# 3. Publish
pnpm changeset publish
# Publishes all changed packages to npm

CI Automation

# .github/workflows/release.yml
name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install

      - name: Create Release PR or Publish
        uses: changesets/action@v1
        with:
          version: pnpm changeset version
          publish: pnpm changeset publish
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Developer Experience Optimization

Path Aliases

// packages/ui/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@ui/*": ["./src/*"]
    }
  }
}

// vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@ui': path.resolve(__dirname, './src')
    }
  }
});

Hot Reload Configuration

// apps/web/vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    watch: {
      // Watch workspace package changes
      ignored: ['!**/node_modules/@myorg/**']
    }
  },
  optimizeDeps: {
    // Exclude internal packages from pre-bundling
    exclude: ['@myorg/ui', '@myorg/utils']
  }
});

VS Code Configuration

// .vscode/settings.json
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "eslint.workingDirectories": [
    { "pattern": "./apps/*" },
    { "pattern": "./packages/*" }
  ],
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/.turbo": true
  }
}
// .vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss"
  ]
}

Common Issues & Solutions

Phantom Dependencies

# .npmrc
shamefully-hoist=false  # Disable dependency hoisting
strict-peer-dependencies=true
auto-install-peers=true

Circular Dependencies

Detecting Circular Dependencies:
┌─────────────────────────────────────────────────────┐
│ @myorg/ui ──────→ @myorg/utils                      │
│     ↑                  │                            │
│     └──────────────────┘  ❌ Circular!              │
│                                                     │
│ Solutions:                                          │
│ 1. Extract common parts to new package              │
│ 2. Use dependency injection                         │
│ 3. Redesign package boundaries                      │
└─────────────────────────────────────────────────────┘
# Detect circular dependencies
pnpm dlx madge --circular packages/*/src/index.ts

Build Order

// turbo.json
{
  "tasks": {
    "build": {
      // ^build ensures dependencies build first
      "dependsOn": ["^build"]
    }
  }
}

Summary

Monorepo makes large project code management more efficient:

AspectPolyrepoMonorepo
Code SharingNeed npm publishInstant sharing
Cross-project ChangesMultiple PRsSingle PR
Dependency ManagementSeparateUnified
CI/CDSeparate configsUnified config
Build SpeedRedundant buildsIncremental cache

Key Takeaways:

  1. pnpm workspace is the foundation of Monorepo
  2. Turborepo dramatically improves build speed through caching
  3. Shared configs reduce duplication, maintain consistency
  4. Changesets solves multi-package version management
  5. Proper package boundary design is key to success

Monorepo isn’t a silver bullet, but for projects with code sharing needs, it’s a powerful architectural choice.


How you organize code determines collaboration efficiency. Choose the architecture that fits your team.