A monorepo lets you share code between apps (web, mobile, docs) and packages (shared utils, configs, UI components) in a single repository. Turborepo + pnpm + TypeScript is the modern stack. Here's how to set it up in 30 minutes.

Why a Monorepo?

ProblemMonorepo Solution
Duplicate tsconfig, ESLint config, etc. in 5 reposOne shared config package. Update once, all apps get it.
Copy-pasting UI components between appsShared UI package. One component, used everywhere.
Can't refactor across apps safelyTypeScript validates ALL consumers when you change a shared package.
CI runs unrelated changes on every commitTurborepo caches tasks. Only changed packages rebuild.

Step-by-Step Setup

# 1. Create the monorepo structure
mkdir my-monorepo && cd my-monorepo
pnpm init

# 2. Create pnpm-workspace.yaml
cat > pnpm-workspace.yaml << 'EOF'
packages:
  - "apps/*"
  - "packages/*"
EOF

# 3. Create directory structure
mkdir -p apps/web apps/docs packages/ui packages/config

# 4. Create root package.json with Turborepo
cat > package.json << 'EOF'
{
  "private": true,
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.5.0"
  }
}
EOF

# 5. Install Turborepo
pnpm install

# 6. Create turbo.json
cat > turbo.json << 'EOF'
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "dev": { "cache": false, "persistent": true },
    "lint": { "dependsOn": ["^build"] },
    "test": { "dependsOn": ["^build"] }
  }
}
EOF

Shared Config Package

# packages/config/package.json
{
  "name": "@repo/config",
  "version": "0.0.0",
  "private": true,
  "exports": {
    "./typescript": "./tsconfig.base.json",
    "./eslint": "./eslint.base.js"
  }
}

# packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

App Configuration

# apps/web/tsconfig.json — each app extends the shared base
{
  "extends": "@repo/config/typescript",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@repo/ui/*": ["../../packages/ui/src/*"]
    }
  },
  "include": ["src", "next-env.d.ts"]
}

Best Practices

  • One package = one purpose. @repo/ui for components, @repo/config for shared configs, @repo/utils for shared utilities. Don't create a "misc" package.
  • Use workspace protocol: In package.json dependencies, use "@repo/ui": "workspace:*" instead of version numbers.
  • Parallel builds: Turborepo runs independent tasks in parallel. A build across 5 packages finishes in the time of the slowest one, not the sum.
  • Remote caching: Turborepo can cache builds remotely (Vercel). CI builds reuse cache from previous CI runs.
  • Don't go monorepo for <3 packages. The overhead isn't worth it for tiny projects. Start with a single repo, extract when you have sharing pain.

Bottom line: Monorepos shine when you have 3+ apps/packages that share code. pnpm workspaces + Turborepo is the best stack in 2026. The shared config package alone saves hours of boilerplate setup per new project. See also: Package Manager Comparison and Build Tools Comparison.