Turborepo Advanced Patterns: Production-Ready Monorepo Architecture in 2025
Master production Turborepo patterns with real-world examples. Migration guide, CI/CD pipelines, remote caching, testing strategies, and framework-specific configs.
Moshiour Rahman
Advertisement
Building a production monorepo requires more than just create-turbo. This guide covers battle-tested patterns from real SaaS projects, including migration strategies, advanced caching, CI/CD optimization, and framework-specific configurations.
What This Guide Covers
| Section | What You’ll Learn |
|---|---|
| Architecture Patterns | Real project structures used in production |
| Migration Guide | Step-by-step polyrepo to monorepo |
| Advanced Caching | Self-hosted S3, cache optimization |
| CI/CD Patterns | GitHub Actions with matrix builds |
| Testing Strategies | Shared configs, coverage aggregation |
| Framework Configs | Next.js, Remix, React Native, Electron |
| Troubleshooting | Common issues and solutions |
Production Architecture Patterns
The SaaS Monorepo Structure
Here’s a battle-tested structure for a multi-product SaaS:
my-saas/
├── apps/
│ ├── web/ # Next.js marketing + app
│ ├── api/ # Hono/Express API server
│ ├── admin/ # Admin dashboard
│ ├── docs/ # Astro documentation
│ └── mobile/ # React Native app
├── packages/
│ ├── ui/ # Shared React components
│ ├── database/ # Prisma/Drizzle + migrations
│ ├── auth/ # Shared auth (Clerk/Auth.js)
│ ├── email/ # React Email templates
│ ├── analytics/ # Tracking abstraction
│ ├── validators/ # Zod schemas (shared API contracts)
│ └── config/ # Environment & constants
├── tooling/
│ ├── eslint/ # Shared ESLint configs
│ ├── typescript/ # Base tsconfigs
│ ├── tailwind/ # Tailwind preset
│ └── prettier/ # Prettier config
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
└── .github/
└── workflows/
├── ci.yml
└── deploy.yml
Dependency Graph Visualization
Understanding your dependency graph is critical for optimization:

Root Configuration Files
turbo.json - Production configuration:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
".env",
".env.local",
".env.production"
],
"globalEnv": [
"NODE_ENV",
"CI",
"VERCEL"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"$TURBO_DEFAULT$",
".env",
".env.local",
".env.production"
],
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**",
"build/**"
]
},
"lint": {
"dependsOn": ["^lint"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^typecheck"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"inputs": [
"src/**",
"tests/**",
"vitest.config.*",
"jest.config.*"
],
"outputs": ["coverage/**"]
},
"test:e2e": {
"dependsOn": ["^build"],
"inputs": ["e2e/**", "playwright.config.*"],
"outputs": ["playwright-report/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
},
"db:generate": {
"cache": false
},
"db:push": {
"cache": false
}
}
}
pnpm-workspace.yaml with catalogs (pnpm 9+):
packages:
- 'apps/*'
- 'packages/*'
- 'tooling/*'
# Centralized dependency versions (pnpm 9+)
catalog:
# React ecosystem
react: ^19.0.0
react-dom: ^19.0.0
next: ^15.1.0
# Validation
zod: ^3.24.0
# Database
drizzle-orm: ^0.38.0
# Development
typescript: ^5.7.0
vitest: ^2.1.0
# Styling
tailwindcss: ^4.0.0
Root package.json:
{
"name": "my-saas",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"test": "turbo run test",
"test:e2e": "turbo run test:e2e",
"clean": "turbo run clean && rm -rf node_modules .turbo",
"format": "prettier --write \"**/*.{ts,tsx,md,json}\"",
"db:generate": "turbo run db:generate",
"db:push": "turbo run db:push"
},
"devDependencies": {
"prettier": "^3.4.0",
"turbo": "^2.3.0"
},
"packageManager": "pnpm@9.15.0",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
}
}
Migration: Polyrepo to Monorepo
Migration Strategy Overview

Step-by-Step Migration
Step 1: Create the monorepo foundation
# Create new monorepo
mkdir my-saas && cd my-saas
pnpm init
# Install Turborepo
pnpm add -D turbo
# Create directory structure
mkdir -p apps packages tooling .github/workflows
# Initialize git
git init
Step 2: Migrate with git history preserved
# Add existing repo as remote
git remote add frontend-repo git@github.com:org/frontend.git
git fetch frontend-repo
# Merge into apps/web with history
git merge frontend-repo/main --allow-unrelated-histories \
-X subtree=apps/web \
-m "chore: migrate frontend to monorepo"
# Repeat for other repos
git remote add api-repo git@github.com:org/api.git
git fetch api-repo
git merge api-repo/main --allow-unrelated-histories \
-X subtree=apps/api \
-m "chore: migrate api to monorepo"
Step 3: Extract shared code
# Create shared packages
mkdir -p packages/ui/src
mkdir -p packages/validators/src
mkdir -p packages/config/src
# Move shared code from apps to packages
# Example: UI components
mv apps/web/src/components/shared/* packages/ui/src/
Step 4: Update package.json files
// apps/web/package.json
{
"name": "@myorg/web",
"dependencies": {
// Use workspace protocol for internal deps
"@myorg/ui": "workspace:*",
"@myorg/validators": "workspace:*",
"@myorg/config": "workspace:*",
// External deps use catalog
"react": "catalog:",
"next": "catalog:"
}
}
Step 5: Fix import paths
// Before (direct file import)
import { Button } from '../../../shared/components/Button';
import { userSchema } from '../../../shared/validators';
// After (package import)
import { Button } from '@myorg/ui';
import { userSchema } from '@myorg/validators';
Migration Checklist
| Task | Status | Notes |
|---|---|---|
| Audit existing dependencies | ☐ | List all unique deps across repos |
| Identify shared code | ☐ | UI, validators, utilities |
| Create monorepo structure | ☐ | apps/, packages/, tooling/ |
| Setup tooling packages | ☐ | ESLint, TypeScript, Prettier |
| Migrate first app with history | ☐ | Usually the frontend |
| Extract shared packages | ☐ | Create packages from shared code |
| Update all imports | ☐ | Replace relative with package imports |
| Configure CI/CD | ☐ | Test build + deploy pipeline |
| Test all apps locally | ☐ | Run dev, build, test |
| Archive old repos | ☐ | Mark as deprecated/archived |
Advanced Caching Strategies
Understanding Cache Behavior

Self-Hosted Remote Cache with S3
Option 1: Using turborepo-remote-cache (Node.js)
// cache-server/src/index.ts
import { createServer } from '@vercel/turbo-remote-cache-server';
import { S3Client } from '@aws-sdk/client-s3';
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const server = createServer({
storage: {
type: 's3',
bucket: process.env.TURBO_CACHE_BUCKET!,
client: s3Client,
},
auth: {
type: 'bearer',
// Generate secure tokens for CI/team
tokens: process.env.TURBO_TOKENS!.split(','),
},
});
server.listen(3001, () => {
console.log('Turbo cache server running on :3001');
});
Option 2: Using Cloudflare R2 (cheaper)
# R2 is S3-compatible and much cheaper for cache
# Set these in CI environment
export TURBO_API="https://your-cache.workers.dev"
export TURBO_TOKEN="your-token"
export TURBO_TEAM="your-team"
// cache-worker/src/index.ts (Cloudflare Worker)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// Verify token
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (token !== env.TURBO_TOKEN) {
return new Response('Unauthorized', { status: 401 });
}
// Handle cache operations
if (request.method === 'GET' && path.startsWith('/v8/artifacts/')) {
const key = path.replace('/v8/artifacts/', '');
const object = await env.TURBO_CACHE.get(key);
if (!object) return new Response('Not found', { status: 404 });
return new Response(object.body);
}
if (request.method === 'PUT' && path.startsWith('/v8/artifacts/')) {
const key = path.replace('/v8/artifacts/', '');
await env.TURBO_CACHE.put(key, request.body);
return new Response('OK');
}
return new Response('Not found', { status: 404 });
},
};
Cache Optimization Tips
// turbo.json - Optimized caching
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
// Only include files that affect build
"src/**/*.{ts,tsx,js,jsx}",
"package.json",
"tsconfig.json",
// Exclude test files from build inputs
"!src/**/*.test.ts",
"!src/**/*.spec.ts",
"!**/__tests__/**"
],
"outputs": ["dist/**"]
},
"lint": {
// Lint rarely needs to depend on built deps
"dependsOn": [],
"inputs": [
"src/**/*.{ts,tsx}",
".eslintrc.*",
"eslint.config.*"
],
// Lint has no outputs (faster cache check)
"outputs": []
},
"typecheck": {
// Typecheck needs built declaration files
"dependsOn": ["^build"],
"inputs": [
"src/**/*.{ts,tsx}",
"tsconfig.json"
],
"outputs": []
}
}
}
Cache Hit Rate Monitoring
# Check cache statistics
turbo run build --summarize
# Output shows cache hit/miss per package
# Tasks: 12 successful, 12 total
# Cached: 10 cached, 2 total (83% hit rate)
# Time: 2.3s (saved 4m 12s)
CI/CD Production Patterns
GitHub Actions: Full Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs for same PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_REMOTE_ONLY: true
jobs:
# Determine affected packages for matrix
changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
web:
- 'apps/web/**'
- 'packages/ui/**'
- 'packages/validators/**'
api:
- 'apps/api/**'
- 'packages/database/**'
- 'packages/validators/**'
admin:
- 'apps/admin/**'
- 'packages/ui/**'
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm turbo run lint --filter='...[origin/main]'
- name: Type Check
run: pnpm turbo run typecheck --filter='...[origin/main]'
test:
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm turbo run test --filter='...[origin/main]'
- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
files: ./packages/*/coverage/lcov.info,./apps/*/coverage/lcov.info
flags: unittests
build:
runs-on: ubuntu-latest
needs: test
strategy:
matrix:
app: [web, api, admin]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build ${{ matrix.app }}
run: pnpm turbo run build --filter=${{ matrix.app }}...
- name: Upload Build Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.app }}-build
path: apps/${{ matrix.app }}/dist
retention-days: 1
e2e:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Install Playwright
run: pnpm exec playwright install --with-deps chromium
- name: Download web build
uses: actions/download-artifact@v4
with:
name: web-build
path: apps/web/dist
- name: Run E2E Tests
run: pnpm turbo run test:e2e --filter=web
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: apps/web/playwright-report/
deploy-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [build, e2e]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: web-build
path: apps/web/dist
- name: Deploy to Vercel Preview
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: apps/web
deploy-production:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
needs: [build, e2e]
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: web-build
path: apps/web/dist
- name: Deploy to Vercel Production
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
working-directory: apps/web
CI Pipeline Visualization

Testing Strategies
Shared Test Configuration
tooling/vitest/base.ts:
import { defineConfig } from 'vitest/config';
export const baseConfig = defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
exclude: [
'node_modules/**',
'dist/**',
'**/*.d.ts',
'**/*.config.*',
'**/index.ts', // Re-exports only
],
},
include: ['src/**/*.{test,spec}.{ts,tsx}'],
passWithNoTests: true,
},
});
tooling/vitest/react.ts:
import { defineConfig, mergeConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { baseConfig } from './base';
export const reactConfig = mergeConfig(
baseConfig,
defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./test/setup.ts'],
},
})
);
packages/ui/vitest.config.ts:
import { reactConfig } from '@myorg/vitest-config/react';
export default reactConfig;
Test Structure Per Package
packages/ui/
├── src/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # Unit test
│ │ └── index.ts
│ └── index.ts
├── test/
│ └── setup.ts # Test setup
├── vitest.config.ts
└── package.json
packages/ui/test/setup.ts:
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
E2E Testing with Playwright
apps/web/playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['github'],
],
use: {
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile',
use: { ...devices['iPhone 14'] },
},
],
webServer: process.env.CI ? undefined : {
command: 'pnpm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Coverage Aggregation
scripts/coverage-report.ts:
import { execSync } from 'child_process';
import { readdirSync, existsSync } from 'fs';
import { join } from 'path';
const packages = [
...readdirSync('packages').map(p => `packages/${p}`),
...readdirSync('apps').map(a => `apps/${a}`),
];
const coverageFiles = packages
.map(p => join(p, 'coverage/lcov.info'))
.filter(existsSync);
if (coverageFiles.length > 0) {
// Merge coverage reports
execSync(`npx lcov-result-merger '*/coverage/lcov.info' merged-coverage.info`);
// Generate HTML report
execSync(`npx genhtml merged-coverage.info -o coverage-report`);
console.log('Coverage report generated at coverage-report/index.html');
}
Framework-Specific Configurations
Next.js 15 App Router
apps/web/next.config.ts:
import type { NextConfig } from 'next';
const config: NextConfig = {
// Enable standalone output for Docker
output: 'standalone',
// Transpile internal packages
transpilePackages: [
'@myorg/ui',
'@myorg/validators',
'@myorg/config',
],
// Experimental features
experimental: {
// Enable React 19 features
reactCompiler: true,
// Optimize package imports
optimizePackageImports: [
'@myorg/ui',
'lucide-react',
],
},
// Configure for monorepo
webpack: (config, { isServer }) => {
// Handle workspace packages
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
};
}
return config;
},
};
export default config;
Hono API Server
apps/api/src/index.ts:
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { db } from '@myorg/database';
import { userRouter } from './routes/user';
const app = new Hono();
// Middleware
app.use('*', logger());
app.use('*', cors({
origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
}));
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }));
// Routes
app.route('/api/users', userRouter);
// Start server
const port = parseInt(process.env.PORT || '4000');
serve({ fetch: app.fetch, port });
console.log(`API running on http://localhost:${port}`);
apps/api/tsconfig.json:
{
"extends": "@myorg/typescript-config/node.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
React Native
apps/mobile/metro.config.js:
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const path = require('path');
// Monorepo root
const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, '../..');
const config = {
watchFolders: [monorepoRoot],
resolver: {
nodeModulesPaths: [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
],
// Handle workspace packages
extraNodeModules: {
'@myorg/ui': path.resolve(monorepoRoot, 'packages/ui'),
'@myorg/validators': path.resolve(monorepoRoot, 'packages/validators'),
'@myorg/config': path.resolve(monorepoRoot, 'packages/config'),
},
},
};
module.exports = mergeConfig(getDefaultConfig(projectRoot), config);
Electron with Vite
apps/desktop/electron.vite.config.ts:
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/main/index.ts'),
},
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/preload/index.ts'),
},
},
},
},
renderer: {
plugins: [react()],
resolve: {
alias: {
'@myorg/ui': resolve(__dirname, '../../packages/ui/src'),
},
},
},
});
Docker with turbo prune
Optimized Multi-Stage Dockerfile
# apps/api/Dockerfile
FROM node:22-alpine AS base
RUN corepack enable
# Prune stage - only copy what's needed
FROM base AS pruner
WORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune @myorg/api --docker
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile
# Build stage
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/ .
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=@myorg/api...
# Production stage
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Don't run as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=builder --chown=appuser:nodejs /app/apps/api/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/apps/api/package.json ./
COPY --from=builder --chown=appuser:nodejs /app/node_modules ./node_modules
USER appuser
EXPOSE 4000
CMD ["node", "dist/index.js"]
Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: myapp
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- '6379:6379'
api:
build:
context: .
dockerfile: apps/api/Dockerfile
target: deps # For development
volumes:
- ./apps/api/src:/app/apps/api/src
- ./packages:/app/packages
ports:
- '4000:4000'
environment:
DATABASE_URL: postgres://dev:dev@postgres:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
- postgres
- redis
volumes:
postgres_data:
Dependency Management
Version Synchronization with Changesets
Setup changesets:
pnpm add -D @changesets/cli
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/validators", "@myorg/config"]
],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@myorg/web", "@myorg/api", "@myorg/admin"]
}
Release workflow:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: pnpm run release
version: pnpm run version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Renovate for Monorepos
renovate.json:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommits",
"group:monorepos"
],
"labels": ["dependencies"],
"packageRules": [
{
"groupName": "React ecosystem",
"matchPackagePatterns": ["^react", "^@types/react"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "Testing",
"matchPackagePatterns": ["vitest", "playwright", "@testing-library"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "Linting",
"matchPackagePatterns": ["eslint", "prettier", "@typescript-eslint"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["patch"],
"automerge": true
}
],
"postUpdateOptions": ["pnpmDedupe"]
}
Troubleshooting Guide
Common Issues Decision Tree

Solutions Table
| Problem | Cause | Solution |
|---|---|---|
| Cache never hits | Outputs not specified | Add "outputs": ["dist/**"] to turbo.json |
| Cache always misses | Env vars not declared | Add env vars to globalEnv or task env |
| Types not found | Missing declaration files | Ensure "declaration": true in tsconfig |
| Workspace package not found | Not in pnpm-workspace.yaml | Add path to workspace config |
| Build order wrong | Missing dependsOn | Add "^build" to dependsOn array |
| Dev server crashes | Circular dependency | Use --graph to identify cycles |
| Hot reload broken | Watch mode not configured | Add "persistent": true to dev task |
| Docker build slow | Not using turbo prune | Use multi-stage build with prune |
| CI timeout | No remote cache | Setup Vercel or self-hosted cache |
| Duplicate dependencies | No pnpm catalogs | Use catalog: for shared versions |
Debug Commands
# Visualize task dependencies
turbo run build --graph
# Check what will run
turbo run build --dry-run
# See cache status
turbo run build --summarize
# Force rebuild (ignore cache)
turbo run build --force
# Run with verbose output
turbo run build --verbosity=2
# See what files are inputs
turbo run build --dry-run=json | jq '.packages[].inputs'
Performance Benchmarks
Real benchmarks from a 10-package monorepo:
| Scenario | Without Turbo | With Turbo | Improvement |
|---|---|---|---|
| Full build (cold) | 4m 32s | 4m 28s | ~1% |
| Full build (local cache) | 4m 32s | 8s | 97% |
| Single package change | 4m 32s | 45s | 83% |
| CI with remote cache | 4m 32s | 12s | 96% |
| Lint (all packages) | 2m 15s | 35s | 74% |
| Typecheck (incremental) | 1m 45s | 15s | 86% |
Cache Size Management
# Check local cache size
du -sh .turbo/cache
# Clear local cache
turbo daemon clean
# Prune old cache entries (keep last 7 days)
find .turbo/cache -type f -mtime +7 -delete
Summary
| Pattern | When to Use |
|---|---|
| pnpm catalogs | Sync versions across all packages |
| workspace:* | Reference internal packages |
| turbo prune | Docker builds with minimal context |
| —filter=[HEAD^1] | CI runs on changed packages only |
| Remote cache | Team collaboration, faster CI |
| Changesets | Publishing packages with changelogs |
| Matrix builds | Parallel CI for multiple apps |
| Project references | TypeScript-aware dependencies |
Essential Commands
# Development
pnpm dev # Start all dev servers
pnpm turbo run dev --filter=web # Start specific app
# Building
pnpm build # Build all
pnpm turbo run build --filter=web... # Build app + deps
# Testing
pnpm test # Test all
pnpm turbo run test --filter='...[HEAD^1]' # Test changed
# CI
pnpm turbo run build lint test --filter='...[origin/main]'
# Maintenance
pnpm turbo run clean # Clean all
turbo daemon clean # Clear cache
Turborepo transforms monorepo development from painful to productive. Start with the basics, add caching, then optimize incrementally based on your bottlenecks.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Turborepo: High-Performance Monorepo Build System
Master Turborepo for monorepo management. Learn workspace setup, caching, pipelines, and build performant multi-package JavaScript projects.
JavaScriptNext.js 14 Tutorial: Complete Guide with App Router
Master Next.js 14 with App Router. Learn server components, data fetching, routing, server actions, and build full-stack React applications.
JavaScriptBuild a Dynamic OG Image Generator for Your Blog (Next.js + Screenshot API)
Stop manually creating social preview images. Learn how to automatically generate unique Open Graph images for every blog post using Next.js and a screenshot API.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.