正在加载,请稍候…

Monorepo Setup Guide: Turborepo, Nx, and pnpm Workspaces Compared

Complete guide to monorepos: when to use them, how to set up Turborepo or Nx with pnpm workspaces, manage shared packages, and optimize CI build performance.

Monorepo vs Polyrepo: Making the Right Choice

A monorepo stores multiple projects in a single repository. Companies like Google, Facebook, and Airbnb use monorepos for their codebases. But that doesn't mean they're right for every team.

Choose a monorepo when:

  • You have shared libraries used by multiple apps
  • Your teams frequently make cross-project changes
  • You want unified tooling, linting, and testing
  • You're building a design system or component library

Stick with polyrepos when:

  • Projects have completely different tech stacks
  • Teams work in complete isolation
  • Security/compliance requires strict separation

pnpm Workspaces: The Foundation

Most modern monorepo tools build on package manager workspaces:

# pnpm-workspace.yaml (root of monorepo)
packages:
  - 'apps/*'       # app-web, app-mobile, app-api
  - 'packages/*'   # ui, utils, config, types
// Root package.json
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}
# Install dependencies for all workspaces
pnpm install

# Add dependency to specific package
pnpm add react --filter @myapp/web

# Add shared internal package
pnpm add @myapp/ui --filter @myapp/web

Turborepo: Fast Cached Builds

Turborepo is the simplest and most popular monorepo build system:

// turbo.json (root)
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],  // Build dependencies first
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,          // Never cache dev servers
      "persistent": true       // Long-running process
    }
  }
}
# Run build for all packages (cached after first run)
turbo build

# Run only for changed packages
turbo build --filter=[HEAD^1]

# Run for a specific app and its dependencies
turbo build --filter=@myapp/web...

# Clear cache
turbo build --force

Turborepo Remote Caching

# Connect to Vercel Remote Cache
npx turbo login
npx turbo link

# Now CI machines share the same cache — 0s builds for unchanged code!
# .github/workflows/ci.yml
- name: Build
  run: turbo build
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Nx: More Powerful, More Complex

Nx is better for large codebases with complex dependency graphs:

# Create new Nx monorepo
npx create-nx-workspace@latest myorg

# Generate apps and libraries
nx generate @nx/react:app web
nx generate @nx/node:app api
nx generate @nx/react:library ui
nx generate @nx/js:library utils

# Run affected tasks (only what changed since main branch)
nx affected --target=build
nx affected --target=test --base=origin/main

# Dependency graph visualization
nx graph
// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "test": {
      "cache": true,
      "inputs": ["default", "^production"]
    }
  },
  "namedInputs": {
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/jest.config.ts"
    ]
  }
}

Shared Package Structure

monorepo/
├── apps/
│   ├── web/                    # Next.js app
│   │   └── package.json        # depends on @myapp/ui, @myapp/utils
│   └── api/                    # Express API  
│       └── package.json        # depends on @myapp/utils, @myapp/types
└── packages/
    ├── ui/                     # Shared React components
    │   ├── src/
    │   │   ├── Button.tsx
    │   │   └── index.ts
    │   ├── package.json
    │   └── tsconfig.json
    ├── utils/                  # Shared utilities
    ├── types/                  # Shared TypeScript types
    └── config/                 # Shared tsconfig, eslint config
// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "0.0.0",
  "main": "./src/index.ts",   // Source directly — no build step needed in dev
  "types": "./src/index.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsc --build",
    "dev": "tsc --watch"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  }
}
// apps/web/package.json
{
  "name": "@myapp/web",
  "dependencies": {
    "@myapp/ui": "workspace:*",    // pnpm workspace reference
    "@myapp/utils": "workspace:*"
  }
}

Shared TypeScript Config

// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

// apps/web/tsconfig.json
{
  "extends": "@myapp/config/tsconfig.base.json",
  "compilerOptions": {
    "lib": ["dom", "ES2022"],
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Shared ESLint Config

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

// apps/web/.eslintrc.js
const base = require('@myapp/config/eslint.base.js');
module.exports = {
  ...base,
  extends: [...base.extends, 'plugin:react/recommended'],
};

Turborepo vs Nx Comparison

Feature Turborepo Nx
Setup complexity Simple Complex
Learning curve Low High
Caching ✅ Excellent ✅ Excellent
Incremental builds
Code generation ❌ Basic ✅ Rich
Plugin ecosystem Growing Mature
Remote caching Vercel (free) Nx Cloud (paid/self-host)
Best for Small-medium Large enterprise

→ Convert package configs between JSON and YAML with the JSON to YAML Converter.