React Monorepo with Turborepo and pnpm Workspaces (2026)
Turborepo is a high-performance build system for JavaScript monorepos. It caches task outputs locally and remotely, runs tasks in parallel respecting dependency order, and integrates with any package manager. Paired with pnpm workspaces, it is the standard setup for teams managing multiple React apps and shared packages in one repository.
Table of Contents
Monorepo Structure
my-monorepo/
├── apps/
│ ├── web/ ← Next.js customer-facing app
│ ├── admin/ ← Vite admin dashboard
│ └── docs/ ← Documentation site
├── packages/
│ ├── ui/ ← Shared React component library
│ ├── config/ ← Shared ESLint, TS, Tailwind configs
│ ├── utils/ ← Shared utility functions
│ └── types/ ← Shared TypeScript types
├── package.json ← Root (no dependencies here)
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.json ← Root TS config (extended by all packages)
pnpm Workspaces
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
# Root package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test",
"typecheck": "turbo typecheck",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.5.0"
},
"packageManager": "pnpm@9.0.0"
}
# Install a package in a specific workspace
pnpm add react --filter web
pnpm add -D vitest --filter admin
# Install shared package into an app
pnpm add @myrepo/ui --filter web --workspace
# In apps/web/package.json: "@myrepo/ui": "workspace:*"
# Run a script in a specific package
pnpm --filter web dev
# Run in all packages matching a pattern
pnpm --filter './apps/*' build
turbo.json Pipeline
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"], // ^ means: run in dependencies first
"inputs": ["$TURBO_DEFAULT$", ".env.production"],
"outputs": [".next/**", "dist/**", "!dist/**/*.map"]
},
"dev": {
"cache": false, // Never cache dev servers
"persistent": true // Long-running process
},
"lint": {
"dependsOn": ["^build"], // Lint after dependencies built
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tests/**", "vitest.config.ts"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"clean": {
"cache": false
}
}
}
// Turborepo caching:
// If inputs haven't changed since last run, turbo replays cached outputs.
// turbo build → runs builds in dependency order, caches results
// turbo build → (second run, no changes) → replays from cache in <1s
// Cache hit rate of 80-90% is common in active monorepos
Shared Packages
// packages/ui/package.json
{
"name": "@myrepo/ui",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts", // Point directly to source (transpiled by consumer)
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@myrepo/config": "workspace:*",
"react": "^18.3.0"
}
}
// packages/ui/src/index.ts — barrel export
export { Button } from './Button'
export { Input } from './Input'
export { Badge } from './Badge'
export { Card } from './Card'
export type { ButtonProps, InputProps } from './types'
// packages/utils/src/index.ts
export { formatCurrency, formatDate, formatNumber } from './format'
export { cn } from './cn' // clsx + tailwind-merge
export { slugify, truncate } from './string'
// Consuming in apps/web — after pnpm add @myrepo/ui --filter web --workspace
import { Button, Card } from '@myrepo/ui'
import { formatCurrency } from '@myrepo/utils'
TypeScript Configuration
// tsconfig.json (root — base config)
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"baseUrl": "."
}
}
// packages/config/tsconfig.base.json — shareable preset
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
// apps/web/tsconfig.json — app-specific config
{
"extends": "@myrepo/config/tsconfig.base.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@myrepo/ui": ["../../packages/ui/src/index.ts"],
"@myrepo/utils": ["../../packages/utils/src/index.ts"]
}
},
"include": ["src", "../../packages/ui/src", "../../packages/utils/src"]
}
// packages/config/eslint-preset.js — shared ESLint
module.exports = {
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended'],
rules: { 'react/react-in-jsx-scope': 'off' },
}
Remote Caching
# Remote cache lets CI and teammates share build artifacts
# Configure Vercel Remote Cache (free with Vercel account)
npx turbo login
npx turbo link # Links repo to Vercel remote cache
# Or self-host with Turborepo Remote Cache Server
# turbo.json
{
"remoteCache": {
"enabled": true,
"apiUrl": "https://cache.mycompany.com"
}
}
# CI — pass TURBO_TOKEN and TURBO_TEAM env vars
# GitHub Actions
- name: Build
run: pnpm turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# With remote cache: a PR that only touches apps/admin
# will get cache hits for packages/ui build → CI goes from 8 min → 90s
CI Integration
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 2 } # Turbo needs git history for affected detection
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- name: Build, lint and test
run: pnpm turbo build lint test typecheck
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# Run only affected packages (experimental)
# turbo build --filter="...[origin/main]"
# This runs build only for packages changed since main branch
# Deploy only affected apps
- name: Deploy web
if: always()
run: pnpm --filter web deploy:prod
# Turbo cache means build step is instant if web hasn't changed
Turborepo vs Nx: Both are excellent monorepo build systems. Turborepo is simpler to configure and integrates seamlessly with Vercel. Nx has more features (code generation, project graph, affected commands built-in) and a richer plugin ecosystem. For most React + Next.js teams, Turborepo is the faster path to productivity.