Skip to main content
← All Articles Β· Β· 12 min read

Turborepo Monorepo Complete Guide 2026: Setup, Caching, and CI/CD

Stop rebuilding everything on every commit. This complete Turborepo guide covers workspace setup, remote caching, pipeline configuration, Nx vs Turborepo comparison, and a full GitHub Actions CI integration β€” with real performance data showing 8-minute builds dropping to 2.3 minutes.

turborepomonorepopnpmci-cdbuild-toolsworkspacedevopsjavascript

Turborepo Monorepo Complete Guide 2026: Setup, Caching, and CI/CD

You’ve hit the wall. Three repositories that share TypeScript types. One change to a shared utility package breaks two apps and you don’t find out until CI fails 15 minutes later. Every pull request triggers a complete rebuild of everything, even when only one package changed.

This is the monorepo migration trigger point for most teams β€” not a philosophy decision, but concrete, daily pain:

  1. Cross-package TypeScript is a nightmare in polyrepos β€” sharing types requires versioning, publishing, and updating across multiple repos
  2. CI rebuilds everything regardless of what changed β€” no caching, no task awareness, no intelligence
  3. Version synchronization between repos is manual work β€” library bumps cascade into 5 separate PRs

Turborepo solves all three. In 2026, it’s the default choice for JavaScript/TypeScript monorepos. This guide covers everything from initial setup to production-grade CI optimization.


What Is a Monorepo?

A monorepo is a single repository that contains multiple packages or applications. The alternative β€” a polyrepo β€” gives each package its own repository.

# Polyrepo (before)
github.com/yourorg/web-app
github.com/yourorg/mobile-app
github.com/yourorg/shared-ui
github.com/yourorg/shared-utils

# Monorepo (after)
github.com/yourorg/platform
  β”œβ”€β”€ apps/
  β”‚   β”œβ”€β”€ web/
  β”‚   └── mobile/
  └── packages/
      β”œβ”€β”€ ui/
      └── utils/

The monorepo doesn’t just consolidate code β€” it enables intelligent build orchestration. Turborepo knows which packages depend on which, and only rebuilds what changed.


Why Turborepo in 2026

The main alternatives are Nx, Lerna (now merged with Nx), and Bazel. Here’s the decision matrix:

ToolBest ForLearning CurveRemote CacheSpeed
TurborepoJS/TS teams, fast adoptionLowβœ… Built-in⚑ Excellent
NxLarge enterprises, Angular/ReactHighβœ… Nx Cloud⚑ Excellent
LernaSimple package managementLow❌ (deprecated solo)πŸ”Ά Moderate
BazelPolyglot (Go, Python, Java)Very Highβœ… Remote🏎️ Maximum

Choose Turborepo when:

  • Your stack is JavaScript/TypeScript
  • You want fast setup without learning a new paradigm
  • Your team is small-to-medium (1-50 engineers)
  • You use Vercel for deployment (Remote Cache integration is seamless)

Choose Nx when:

  • You have 50+ engineers
  • You need advanced code generation, affected-graph visualization, and enterprise support
  • You’re in a large Angular or React Native shop

Installation and Initial Setup

Prerequisites

  • Node.js 18+
  • pnpm 8+ (recommended) or npm/yarn

Create a New Turborepo Project

npx create-turbo@latest my-platform
cd my-platform

This scaffolds the recommended structure. For an existing project, install manually:

pnpm add turbo --save-dev -w

Project Structure

my-platform/
β”œβ”€β”€ apps/
β”‚   β”œβ”€β”€ web/                 # Next.js app
β”‚   └── docs/                # Docusaurus or similar
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ ui/                  # Shared React components
β”‚   β”œβ”€β”€ typescript-config/   # Shared tsconfig
β”‚   └── eslint-config/       # Shared ESLint config
β”œβ”€β”€ turbo.json               # Turborepo configuration
β”œβ”€β”€ package.json             # Root package.json
└── pnpm-workspace.yaml      # pnpm workspace declaration

Workspace Configuration

pnpm-workspace.yaml

packages:
  - "apps/*"
  - "packages/*"

Root package.json

{
  "name": "my-platform",
  "private": true,
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test",
    "clean": "turbo clean"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  },
  "packageManager": "[email protected]"
}

Package package.json (shared UI example)

{
  "name": "@myplatform/ui",
  "version": "0.0.1",
  "private": true,
  "exports": {
    ".": {
      "import": "./src/index.tsx",
      "types": "./src/index.tsx"
    }
  },
  "scripts": {
    "build": "tsc",
    "lint": "eslint . --max-warnings 0",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@myplatform/typescript-config": "*",
    "@myplatform/eslint-config": "*"
  }
}

App package.json (Next.js app consuming shared package)

{
  "name": "web",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "@myplatform/ui": "*",
    "next": "^14.0.0",
    "react": "^18.0.0"
  }
}

The "*" workspace protocol means pnpm resolves the dependency from the local workspace, not npm.


Pipeline Configuration (turbo.json)

This is Turborepo’s core config β€” where you define task dependencies and caching behavior.

Basic turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**", "*.json"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "dependsOn": [],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "*.json"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "clean": {
      "cache": false
    }
  }
}

Understanding dependsOn

The ^build notation means β€œrun build in all dependencies first”:

@myplatform/ui builds first
  β†’ web builds after (it depends on @myplatform/ui)
  β†’ docs builds after

Without ^build, tasks run in any order and you’ll get import errors when the dependent package hasn’t compiled yet.

Configuring Outputs for Caching

Correct output configuration is critical for cache hits:

{
  "tasks": {
    "build": {
      "outputs": [
        ".next/**",
        "!.next/cache/**",
        "dist/**",
        "build/**"
      ]
    }
  }
}

Turborepo hashes inputs and stores outputs. Next time the same inputs are detected, it restores outputs from cache β€” skipping the build entirely.


Remote Caching

Local cache saves time for individual developers. Remote cache saves time across your entire team and CI.

Enable Vercel Remote Cache

# Login to Vercel
npx turbo login

# Link to your Vercel account
npx turbo link

From this point, every developer and every CI run shares the same cache. If engineer A builds a package with the same inputs as engineer B, B gets the cached output β€” instantly.

Performance Reality Check

Real-world data from a medium-sized Next.js + React Native monorepo:

ScenarioBefore TurborepoAfter Remote Cache
Full CI build (no cache)8 min 12 sec8 min 12 sec
PR with 1 package changed8 min 12 sec2 min 18 sec
PR with no package changed8 min 12 sec47 sec
Local dev after colleague built3 min 40 sec12 sec

The 8min β†’ 2.3min reduction comes from skipping unchanged packages. When only the web app changed, Turborepo skips rebuilding @myplatform/ui and @myplatform/utils β€” because their inputs haven’t changed and their outputs are cached.

Self-Hosted Remote Cache (Turborepo 2.x)

In Turborepo 2.x, you can use any S3-compatible storage as a remote cache:

TURBO_REMOTE_CACHE_SIGNATURE_KEY=<your-key> \
TURBO_API=https://your-cache-server.com \
turbo build --remote-cache-timeout 60

Open-source options: ducktape, turborepo-remote-cache.


Turborepo 2.x New Features (2025-2026)

Watch Mode Improvements

# Turborepo 2.x β€” significantly improved watch mode
turbo watch build lint

# Processes only affected packages on file change
# Previously required --filter flags and manual management

Dry Run Output (Better CI Visibility)

turbo build --dry-run

# Output:
# β€’ Packages in scope: web, @myplatform/ui
# β€’ Running build in 2 packages
# β€’ web#build: cache miss, executing
# β€’ @myplatform/ui#build: cache hit, replaying

Filter Syntax Improvements

# Build only changed packages since last commit
turbo build --filter='[HEAD^1]'

# Build a specific package and its dependencies
turbo build --filter=web...

# Build everything that depends on ui package
turbo build --filter=...@myplatform/ui

CI/CD Integration (GitHub Actions)

Complete GitHub Actions Workflow

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-test:
    name: Build & Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Required for Turborepo affected detection

      - name: Setup pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 9

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm turbo build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

      - name: Test
        run: pnpm turbo test
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

      - name: Lint
        run: pnpm turbo lint
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

Setting Up CI Secrets

In your Vercel dashboard:

  1. Go to your team β†’ Settings β†’ Remote Cache
  2. Copy your Team ID and Token
  3. Add to GitHub repository secrets: TURBO_TOKEN and TURBO_TEAM

Shared TypeScript Configuration

One of the biggest wins in a monorepo is centralizing TypeScript config.

packages/typescript-config/package.json

{
  "name": "@myplatform/typescript-config",
  "version": "0.0.1",
  "private": true,
  "license": "MIT",
  "files": ["base.json", "nextjs.json", "react-library.json"]
}

packages/typescript-config/base.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "incremental": false,
    "isolatedModules": true,
    "lib": ["es2022", "DOM", "DOM.Iterable"],
    "module": "NodeNext",
    "moduleDetection": "force",
    "moduleResolution": "NodeNext",
    "noUncheckedIndexedAccess": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  }
}

packages/typescript-config/nextjs.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Next.js",
  "extends": "./base.json",
  "compilerOptions": {
    "allowJs": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "noEmit": true,
    "plugins": [{ "name": "next" }],
    "target": "ES2017"
  }
}

Each app then extends this centralized config:

// apps/web/tsconfig.json
{
  "extends": "@myplatform/typescript-config/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["src/**/*", "next-env.d.ts"],
  "exclude": ["node_modules"]
}

Migrating an Existing Multi-Repo Setup

Step-by-Step Migration

1. Create the monorepo structure

npx create-turbo@latest my-platform --example with-tailwind

2. Move existing repositories

# Copy each repo into apps/ or packages/
cp -r ../old-web-repo apps/web
cp -r ../old-shared-ui packages/ui

# Remove old .git directories from moved repos
rm -rf apps/web/.git packages/ui/.git

3. Update package names and internal dependencies

# In packages/ui/package.json
{ "name": "@myplatform/ui" }  # was: "shared-ui"

# In apps/web/package.json β€” replace external dependency with workspace reference
{ "dependencies": { "@myplatform/ui": "*" } }  # was: "^1.4.2" from npm

4. Merge lock files

# Delete all existing lock files
find . -name "package-lock.json" -not -path "*/node_modules/*" -delete
find . -name "yarn.lock" -not -path "*/node_modules/*" -delete

# Install fresh from root
pnpm install

5. Add turbo.json and verify

pnpm turbo build --dry-run

Common Issues and Solutions

Issue: TypeScript can’t find types from a workspace package

Error: Cannot find module '@myplatform/ui'

Fix: Make sure the package has a correct exports field and the consuming app’s TypeScript config includes the workspace package path.

// packages/ui/package.json
{
  "exports": {
    ".": {
      "types": "./src/index.tsx",
      "import": "./src/index.tsx"
    }
  }
}

Issue: Cache isn’t being hit

Fix: Check that your outputs array in turbo.json matches the actual build output paths. A mismatch means outputs aren’t stored/restored correctly.

# Debug cache behavior
turbo build --verbosity=2

Issue: turbo dev exits immediately

Fix: Add "persistent": true to the dev task in turbo.json. Persistent tasks run until killed (servers, watchers) β€” without this flag, Turborepo treats them as completed when the process starts.


FAQ

Q: Should I use pnpm, npm, or yarn workspaces? A: pnpm is the recommended choice. It’s faster, uses less disk space due to content-addressable storage, and has the best workspace protocol support. npm workspaces work but are slower. Yarn Classic (v1) workspaces are legacy and not recommended for new projects.

Q: Can Turborepo handle monorepos with non-JavaScript packages? A: Turborepo is JavaScript/TypeScript-first. For polyglot monorepos (Go + TypeScript, Python + JS), look at Bazel or Nx which have first-class support for multiple languages.

Q: How does Turborepo compare to just using --workspace flags in npm/pnpm? A: Workspace flags handle dependency management (sharing node_modules). Turborepo handles task orchestration (intelligent build ordering, caching, parallel execution). They solve different problems and work together.

Q: Is Remote Cache secure? Can anyone access my build outputs? A: Turborepo Remote Cache artifacts are signed with your team token. The cache content is tied to your Vercel team and requires authentication. For enterprise security requirements, use a self-hosted remote cache server.

Q: What’s the performance ceiling of Turborepo caching? A: Once a task’s inputs haven’t changed, the cache hit approaches 0ms task time (just the restoration time). In practice, large projects see 70-90% cache hit rates in CI after the first run, turning multi-minute builds into sub-minute operations.


Summary

Turborepo in 2026 is the straightforward answer to the most common JavaScript monorepo problems:

Pain PointTurborepo Solution
Cross-package TypeScriptWorkspace protocol + shared tsconfig package
Slow CI (rebuilding everything)Intelligent pipeline + Remote Cache
Version synchronizationSingle repo, single pnpm install
Developer experienceParallel tasks, watch mode, dry-run visibility

The setup investment (2-4 hours for a medium project migration) pays off on the first PR where CI runs in 2 minutes instead of 8.


Building something with Turborepo? Check out our pnpm Workspaces guide and CI/CD best practices for the full picture.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.