Turborepo - Complete Guide to High-Performance Monorepo Build System
What is Turborepo?
Turborepo is a high-performance build system designed specifically for JavaScript and TypeScript monorepos. Acquired by Vercel in 2021, it has become one of the most popular tools for managing large codebases. The core idea behind Turborepo is to speed up builds through intelligent caching, parallel execution, and incremental builds.
In a traditional approach, building a monorepo means running the same tasks repeatedly, even when the code hasn't changed. Turborepo solves this problem by remembering the results of previous builds and skipping unnecessary work. The result? Builds that used to take minutes now finish in seconds.
Why Turborepo?
Key advantages of Turborepo
- Intelligent caching - Doesn't rebuild what's already built
- Parallel execution - Utilizes all CPU cores
- Remote caching - Share cache between team and CI
- Incremental builds - Build only what changed
- Pipeline definition - Define dependencies between tasks
- Zero config - Works with npm, pnpm, yarn workspaces
- Vercel integration - Native Vercel integration
- Pruned subsets - Deploy only the packages you need
Turborepo vs Other Build Systems
| Feature | Turborepo | Nx | Lerna | Rush |
|---|---|---|---|---|
| Caching | Local + Remote | Local + Remote | None (plugin) | Local |
| Setup | Minimal | Complex | Simple | Complex |
| Learning curve | Easy | Medium | Easy | Hard |
| Remote cache | Vercel (free) | Nx Cloud | None | Azure |
| Ecosystem | Growing | Large | Legacy | Enterprise |
| Task runner | Built-in | Built-in | npm/yarn | Custom |
| Price | Free + Vercel tiers | Free + Cloud tiers | Free | Free |
When to choose Turborepo?
- Speed is a priority - fastest time to a working build
- Already using Vercel - native integration
- Simple setup - minimal configuration
- Using npm/pnpm/yarn workspaces - zero migration effort
- Need remote cache - sharing between CI and developers
Installation and Configuration
New project
# Create a new monorepo with Turborepo
npx create-turbo@latest my-monorepo
# Or with a specific template
npx create-turbo@latest my-monorepo --example with-tailwind
npx create-turbo@latest my-monorepo --example kitchen-sink
# With pnpm
pnpm dlx create-turbo@latest my-monorepoAdding to an existing project
# Install Turborepo
npm install turbo --save-dev
# Or globally
npm install turbo --global
# With pnpm
pnpm add turbo --save-dev --workspace-rootProject structure
my-monorepo/
βββ apps/
β βββ web/ # Next.js app
β β βββ src/
β β βββ package.json
β β βββ tsconfig.json
β βββ docs/ # Documentation
β β βββ src/
β β βββ package.json
β βββ admin/ # Admin panel
β βββ package.json
βββ packages/
β βββ ui/ # Shared UI components
β β βββ src/
β β βββ package.json
β β βββ tsconfig.json
β βββ config-eslint/ # Shared ESLint config
β β βββ package.json
β βββ config-typescript/ # Shared TS config
β β βββ package.json
β βββ utils/ # Shared utilities
β βββ src/
β βββ package.json
βββ package.json # Root package.json
βββ turbo.json # Turborepo config
βββ pnpm-workspace.yaml # Workspace config (pnpm)
βββ .gitignoreRoot package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test",
"clean": "turbo clean",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"turbo": "^2.0.0",
"prettier": "^3.0.0"
},
"packageManager": "pnpm@8.15.0"
}turbo.json - Main configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"env": ["DATABASE_URL", "API_KEY"]
},
"lint": {
"dependsOn": ["^lint"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"]
},
"clean": {
"cache": false
}
}
}Pipelines and Tasks
Understanding the Pipeline
The pipeline defines relationships between tasks in a monorepo:
{
"tasks": {
"build": {
// ^ means "build dependencies first"
"dependsOn": ["^build"],
// Output files to cache
"outputs": ["dist/**", ".next/**"]
},
"test": {
// Depends on local build (without ^)
"dependsOn": ["build"],
// Input files that affect the cache
"inputs": ["src/**", "test/**"]
},
"deploy": {
// Depends on build and test of the same package
"dependsOn": ["build", "test"]
}
}
}Dependency types
{
"tasks": {
"build": {
// ^build - build dependencies first (topological)
"dependsOn": ["^build"]
},
"test": {
// build - build THIS package first (same package)
"dependsOn": ["build"]
},
"deploy": {
// Both types together
"dependsOn": ["^build", "test", "lint"]
},
"e2e": {
// Dependency on a specific package
"dependsOn": ["web#build"]
}
}
}Outputs and Caching
{
"tasks": {
"build": {
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**", // Exclude cache
"build/**"
]
},
"lint": {
// No outputs = task doesn't produce files
"outputs": []
},
"test": {
"outputs": ["coverage/**"]
}
}
}Inputs - controlling cache invalidation
{
"tasks": {
"build": {
// Only these files affect the cache
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"package.json",
"tsconfig.json"
]
},
"lint": {
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
".eslintrc.js",
"package.json"
]
},
"test": {
"inputs": [
"$TURBO_DEFAULT$", // Default inputs
"jest.config.js",
"test/**"
]
}
}
}Environment Variables
{
// Global env - affects all tasks
"globalEnv": ["CI", "NODE_ENV"],
// Global dependencies - files affecting everything
"globalDependencies": [".env", "tsconfig.base.json"],
"tasks": {
"build": {
// Task-specific env
"env": ["DATABASE_URL", "API_KEY", "NEXT_PUBLIC_*"],
"passThroughEnv": ["AWS_SECRET_KEY"] // Doesn't affect cache
}
}
}Running Tasks
Basic commands
# Run task in all packages
turbo build
turbo lint
turbo test
# Dev mode (no cache, persistent)
turbo dev
# Verbose output
turbo build --verbosity=2
# Dry run - show what will be executed
turbo build --dry-run
# Show dependency graph
turbo build --graph
turbo build --graph=graph.pngFiltering packages
# Only a specific package
turbo build --filter=web
turbo build --filter=@repo/ui
# Package and its dependencies
turbo build --filter=web...
# Package and its dependents (packages that depend on it)
turbo build --filter=...@repo/ui
# Packages in a specific folder
turbo build --filter="./apps/*"
turbo build --filter="./packages/*"
# Exclude packages
turbo build --filter="!docs"
# Combinations
turbo build --filter="web..." --filter="!docs"
# Only changed since last commit
turbo build --filter="[HEAD^1]"
turbo build --filter="...[main...HEAD]"Advanced filters
# Packages changed in PR
turbo build --filter="[origin/main...HEAD]"
# Packages depending on changed ui
turbo build --filter="...@repo/ui[HEAD^1]"
# All apps
turbo build --filter="./apps/**"
# Packages with a specific tag in package.json
turbo build --filter="@repo/*"Execution options
# Limit parallel tasks
turbo build --concurrency=4
turbo build --concurrency=50% # 50% CPU cores
# Continue despite errors
turbo build --continue
# Force rebuild (ignore cache)
turbo build --force
# Output logs
turbo build --output-logs=full
turbo build --output-logs=hash-only
turbo build --output-logs=new-only
turbo build --output-logs=errors-onlyRemote Caching
Configuration with Vercel
# Login to Vercel
npx turbo login
# Link your project
npx turbo link
# Now cache is shared!
turbo buildVerifying Remote Cache
# Check status
turbo build --summarize
# Output will show:
# Cache: 5 cached, 2 computed
# Remote: 3 downloaded, 2 uploadedSelf-hosted Remote Cache
// Custom cache server (Express example)
import express from 'express'
import { createHash } from 'crypto'
const app = express()
const cache = new Map<string, Buffer>()
// GET artifact
app.get('/v8/artifacts/:hash', (req, res) => {
const artifact = cache.get(req.params.hash)
if (artifact) {
res.send(artifact)
} else {
res.status(404).send('Not found')
}
})
// PUT artifact
app.put('/v8/artifacts/:hash', express.raw({ limit: '50mb' }), (req, res) => {
cache.set(req.params.hash, req.body)
res.status(200).send('OK')
})
app.listen(3001)# Using a custom server
turbo build --api="http://localhost:3001" --token="secret"CI/CD Configuration
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
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
- run: pnpm build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Workspace Packages
UI component package
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./styles.css": "./styles.css"
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"test": "vitest run"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}// packages/ui/src/index.ts
export { Button } from './button'
export { Card } from './card'
export { Input } from './input'
export type { ButtonProps, CardProps, InputProps } from './types'Shared Config Packages
// packages/config-eslint/package.json
{
"name": "@repo/config-eslint",
"version": "0.0.0",
"private": true,
"main": "index.js",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.0.0",
"eslint-plugin-react-hooks": "^4.0.0"
}
}// packages/config-eslint/index.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': 'warn'
},
settings: {
react: { version: 'detect' }
}
}Using shared packages
// apps/web/package.json
{
"name": "web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint . --ext .ts,.tsx"
},
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^14.0.0",
"react": "^18.0.0"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*"
}
}// apps/web/.eslintrc.js
module.exports = {
extends: ['@repo/config-eslint'],
parserOptions: {
project: './tsconfig.json'
}
}TypeScript Config Sharing
// packages/config-typescript/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
}
}// packages/config-typescript/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"module": "ESNext",
"target": "ES2022",
"jsx": "preserve",
"plugins": [{ "name": "next" }]
}
}// apps/web/tsconfig.json
{
"extends": "@repo/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}Package-specific Tasks
Overriding tasks per-package
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
// Override for a specific package
"web#build": {
"dependsOn": ["^build"],
"outputs": [".next/**"],
"env": ["NEXT_PUBLIC_API_URL"]
},
// Override for docs
"docs#build": {
"dependsOn": ["^build"],
"outputs": [".docusaurus/**", "build/**"]
}
}
}Local turbo.json in a package
// apps/web/turbo.json
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"], // Inherit from root turbo.json
"tasks": {
"build": {
// Override only specific options
"env": ["ANALYZE", "NEXT_PUBLIC_SENTRY_DSN"]
},
"dev": {
"persistent": true,
"cache": false
}
}
}Generators (Codegen)
Creating a generator
# Create generators folder
mkdir -p turbo/generators// turbo/generators/config.ts
import type { PlopTypes } from '@turbo/gen'
export default function generator(plop: PlopTypes.NodePlopAPI): void {
// New package generator
plop.setGenerator('package', {
description: 'Create a new package',
prompts: [
{
type: 'input',
name: 'name',
message: 'Package name:'
},
{
type: 'list',
name: 'type',
message: 'Package type:',
choices: ['lib', 'config', 'tool']
}
],
actions: [
{
type: 'add',
path: 'packages/{{name}}/package.json',
templateFile: 'templates/package.json.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/src/index.ts',
templateFile: 'templates/index.ts.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/tsconfig.json',
templateFile: 'templates/tsconfig.json.hbs'
}
]
})
// New app generator
plop.setGenerator('app', {
description: 'Create a new application',
prompts: [
{
type: 'input',
name: 'name',
message: 'Application name:'
},
{
type: 'list',
name: 'framework',
message: 'Framework:',
choices: ['next', 'remix', 'astro']
}
],
actions: (data) => {
const actions: PlopTypes.ActionType[] = []
if (data?.framework === 'next') {
actions.push({
type: 'addMany',
destination: 'apps/{{name}}',
templateFiles: 'templates/next/**/*',
base: 'templates/next'
})
}
return actions
}
})
}Using the generator
# List available generators
turbo gen
# Run a specific generator
turbo gen package
turbo gen app
# With arguments
turbo gen package --name=my-lib --type=libTemplates
{
"name": "@repo/",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"test": "vitest run"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}Pruned Deployments
Docker with Pruned Monorepo
# Dockerfile
FROM node:20-alpine AS base
# Pruner stage
FROM base AS pruner
RUN npm install -g turbo
WORKDIR /app
COPY . .
RUN turbo prune web --docker
# Installer stage
FROM base AS installer
WORKDIR /app
# Install only needed dependencies
COPY /app/out/json/ .
COPY /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Builder stage
FROM base AS builder
WORKDIR /app
COPY /app/ .
COPY /app/out/full/ .
RUN corepack enable pnpm && pnpm turbo build --filter=web
# Runner stage
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY /app/apps/web/.next/standalone ./
COPY /app/apps/web/.next/static ./apps/web/.next/static
COPY /app/apps/web/public ./apps/web/public
CMD ["node", "apps/web/server.js"]Pruning for CI
# Create pruned subset
turbo prune web --docker
# Result in out/:
# out/
# βββ json/ # package.json files
# βββ full/ # Full source code
# βββ pnpm-lock.yaml # Pruned lockfileDebugging and Troubleshooting
Cache Debugging
# Show why a task was executed
turbo build --summarize
# Show hash computation
turbo build --dry-run=json | jq
# Force cache miss
turbo build --force
# Clear cache
turbo clean
rm -rf node_modules/.cache/turboTask Graph
# Generate dependency graph
turbo build --graph=graph.html
turbo build --graph=graph.png
turbo build --graph=graph.json
# Show in console
turbo build --graphVerbose Logging
# Verbosity levels
turbo build --verbosity=0 # Quiet
turbo build --verbosity=1 # Default
turbo build --verbosity=2 # Verbose
# Show all logs
turbo build --output-logs=full
# Only errors
turbo build --output-logs=errors-onlyCommon Issues
# Problem: Cache not working
# Solution: Check inputs and outputs
# Problem: Task runs despite no changes
# Check env variables
turbo build --summarize | grep -A 10 "environment"
# Problem: Circular dependency
turbo build --graph # Visualize dependencies
# Problem: Slow builds
turbo build --profile=profile.json
# Analyze in Chrome DevToolsBest Practices
Package structure
packages/
βββ core/ # Business logic (no UI)
βββ ui/ # Shared React components
βββ hooks/ # Shared React hooks
βββ utils/ # Helpers, formatters
βββ types/ # Shared TypeScript types
βββ config-*/ # Shared configs (eslint, ts, prettier)
βββ api-client/ # Generated API clientNaming Conventions
// Use scope for packages
{
"name": "@repo/ui", // β
"name": "@mycompany/ui", // β
"name": "ui", // β Conflicts with npm packages
}Dependency Management
// Root package.json - shared devDependencies
{
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.0.0",
"prettier": "^3.0.0"
}
}
// Package - only specific deps
{
"dependencies": {
"@repo/ui": "workspace:*" // Internal dependency
},
"devDependencies": {
"@repo/config-eslint": "workspace:*"
}
}Task Organization
{
"tasks": {
// Build pipeline
"build": { "dependsOn": ["^build"] },
"build:production": { "dependsOn": ["^build", "lint", "test"] },
// Dev
"dev": { "cache": false, "persistent": true },
// Quality
"lint": { "outputs": [] },
"lint:fix": { "outputs": [], "cache": false },
"typecheck": { "outputs": [] },
"test": { "dependsOn": ["build"] },
"test:watch": { "cache": false, "persistent": true },
// Maintenance
"clean": { "cache": false }
}
}Integrations
Vercel Deployment
// vercel.json
{
"buildCommand": "pnpm turbo build --filter=web",
"installCommand": "pnpm install",
"framework": "nextjs",
"outputDirectory": "apps/web/.next"
}GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Needed for --filter=[HEAD^1]
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm turbo build
- name: Test
run: pnpm turbo test
- name: Lint
run: pnpm turbo lintChangesets (versioning)
npm install @changesets/cli -D
npx changeset init// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@repo/config-*"]
}FAQ - Frequently Asked Questions
Does Turborepo require Vercel?
No! Turborepo is open-source and works without Vercel. Remote caching can be self-hosted or you can use Vercel as a convenient option.
How do I migrate from Lerna/Nx?
Turborepo is additive - you can add turbo.json to an existing monorepo without removing other tools. Gradually move tasks to Turborepo.
Can I use npm instead of pnpm?
Yes! Turborepo works with npm workspaces, pnpm workspaces, and yarn workspaces. pnpm is recommended for performance reasons.
How do I debug caching issues?
Use turbo build --summarize to see what affects the cache hash. Check env variables, inputs, and outputs in turbo.json.
Does Turborepo support monorepos with different languages?
Turborepo is optimized for JavaScript/TypeScript but can orchestrate any tasks. For polyglot monorepos, consider Nx or Bazel.
What's the difference between dependsOn: ["^build"] and dependsOn: ["build"]?
^build- build dependencies first (packages we depend on)build- build in the same package first
How to optimize CI build time?
- Enable remote caching
- Use
--filterfor changed packages - Parallel jobs in CI
- Optimize inputs/outputs
Summary
Turborepo is a powerful tool for managing monorepos that offers:
- Intelligent caching - local and remote
- Parallel execution - maximum CPU utilization
- Minimal config - works out-of-the-box
- Vercel integration - native integration
- Incremental adoption - easy migration
Ideal for teams looking for a simple yet powerful monorepo solution without unnecessary complexity.