Guide

Turborepo fundamentals explained

A monorepo with twenty packages can waste ten minutes on every pull request re-running lint and tests in folders nobody touched. Turborepo is a high-performance build system for JavaScript and TypeScript monorepos: it understands task dependencies, fingerprints inputs, and caches outputs so unchanged work is skipped locally and in CI. It does not replace pnpm or npm — Turborepo orchestrates the scripts those tools install. Pair it with our monorepo architecture guide for boundaries and CI strategy. This guide covers installation, turbo.json pipelines, task graphs, local and remote caching, filtering, integration with pnpm workspaces, a Harbor Commerce build graph worked example, a tooling decision table, common pitfalls, and a production checklist.

What Turborepo is (and what it is not)

Turborepo sits above your package manager and below CI. When you run turbo run build, it:

  • Discovers every workspace package that defines a build script.
  • Reads turbo.json to learn which tasks depend on which (e.g. app build waits for library build).
  • Hashes declared inputs (source files, env vars, lockfile snippets) per task.
  • Restores cached outputs from disk or a remote cache when the hash matches a prior run.
  • Schedules remaining tasks in parallel up to your concurrency limit.

Turborepo is not a bundler ( Vite, Webpack), a test runner ( Vitest), or a replacement for Nx’s code generators. It focuses on one problem: running the right tasks in the right order as fast as possible. Vercel maintains Turborepo; remote caching integrates with Vercel but works on any CI with an access token.

Installation and first turbo.json

Add Turborepo as a root dev dependency in a monorepo that already uses pnpm, npm, or Yarn workspaces:

pnpm add -D turbo -w

# Scaffold turbo.json interactively
pnpm dlx create-turbo@latest --example with-docker

A minimal root turbo.json declares global settings and a pipeline (task definitions):

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

Key fields:

  • dependsOn^build means “run build in all workspace dependencies first.” Without ^, the dependency is within the same package.
  • outputs — globs Turborepo stores in the cache. Missing outputs means cache hits restore nothing useful.
  • cache: false — for long-running dev servers that should never replay from cache.
  • persistent: true — keeps the process alive (watch mode).
  • globalDependencies — files that invalidate every task when changed (root env, shared tsconfig).

Root package.json scripts delegate to turbo:

{
  "scripts": {
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "dev": "turbo run dev"
  }
}

Task graphs and package boundaries

Turborepo builds a directed acyclic graph (DAG) from dependsOn. Suppose apps/dashboard depends on packages/ui-kit and packages/pricing-core. Running turbo run build --filter=dashboard schedules:

  1. pricing-core#build and ui-kit#build (no upstream deps).
  2. dashboard#build after both complete.

If only ui-kit source changed since the last cache, Turborepo replays pricing-core#build from cache and rebuilds only ui-kit plus dashboard. This is stricter and faster than pnpm run -r build, which blindly runs every package script.

inputs and env passthrough

By default Turborepo hashes all files in a package except node_modules and prior outputs. Narrow inputs for noisy folders:

"build": {
  "inputs": ["src/**", "tsconfig.json", "package.json"],
  "outputs": ["dist/**"]
}

Environment variables that affect build output must be listed or cache hits leak wrong bundles across environments:

"build": {
  "env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"],
  "passThroughEnv": ["AWS_SECRET_ACCESS_KEY"]
}

passThroughEnv exposes secrets to the task without including them in the cache key (use when the var does not change output bytes).

Local caching

Turborepo writes artifacts to node_modules/.cache/turbo by default. A cache hit prints cache hit, replaying logs and finishes in milliseconds. Cache misses run the real script and store stdout plus output globs.

Useful CLI flags:

turbo run build              # read + write local cache
turbo run build --force        # ignore cache, always execute
turbo run build --dry-run      # print execution plan without running
turbo run build --summarize    # JSON report of cache hits/misses

Clean stale artifacts with turbo run build --force once after changing outputs globs in turbo.json — old cache entries may not match new paths.

Remote caching across CI and teammates

Local caches do not help a fresh GitHub Actions runner. Remote cache uploads task outputs to Vercel (or a self-hosted API-compatible server) so the next PR or teammate restores the same artifacts.

# One-time login (developer laptop)
pnpm dlx turbo login
pnpm dlx turbo link

# CI: set environment variables
TURBO_TOKEN=<team-token>
TURBO_TEAM=<team-slug>

With tokens set, turbo run build in CI reads remote cache before executing. A developer who already built pricing-core on main can save CI six minutes when their feature branch only touches the dashboard. Remote cache keys include task hash, not branch name — shared mainline builds accelerate feature work.

Self-hosting is possible via Turborepo’s remote cache API for teams that cannot use Vercel. Document retention policy: cached bundles may contain compiled env-specific constants; restrict token scope to CI and trusted developers.

Filtering and pnpm integration

Turborepo respects workspace package names. Combine with pnpm filters for surgical CI:

# Build dashboard and its dependencies
turbo run build --filter=@harbor/dashboard

# Build only packages changed since main
turbo run test --filter=...[origin/main]

# Build a package and all dependents (reverse graph)
turbo run build --filter=...@harbor/ui-kit

In GitHub Actions, fetch depth 0 so ...[origin/main] resolves correctly. Our GitHub Actions fundamentals guide covers checkout and cache setup; pair actions/cache on node_modules/.cache/turbo with remote cache for belt-and-suspenders speed on cache provider outages.

Package manager choice is independent: Turborepo detects pnpm, npm, Yarn, and Bun lockfiles for hashing. Keep one lockfile; see the pnpm guide for workspace protocol details.

Worked example: Harbor Commerce build graph

Harbor Commerce runs four apps and nine shared packages on every merge to main. Before Turborepo, pnpm run -r build took fourteen minutes in CI even when a PR changed one line in the marketing site.

Pipeline design

  • packages/pricing-core#build — emits dist/; no upstream tasks.
  • packages/ui-kit#build — depends on ^build; Storybook static export to storybook-static/.
  • apps/dashboard#build — Vite production bundle; depends on both libraries.
  • apps/pricing-api#build — TypeScript compile only; depends on pricing-core.
// turbo.json excerpt
"tasks": {
  "build": {
    "dependsOn": ["^build"],
    "outputs": ["dist/**", "storybook-static/**"]
  },
  "typecheck": {
    "dependsOn": ["^build"],
    "outputs": []
  }
}

CI job sequence: pnpm install --frozen-lockfile, then turbo run lint typecheck test build --filter=...[origin/main]. Docs-only PRs skip all app builds. UI kit changes rebuild kit plus dashboard but replay pricing-api from remote cache. Average PR CI dropped from fourteen minutes to four; main-branch nightly still runs full turbo run build --force weekly to catch drift.

Monorepo orchestration decision table

NeedTurborepoNxpnpm -r onlyBazel
Fast JS/TS task caching with minimal config Best fit Heavier but more features No caching Steep learning curve
Remote cache for CI Built-in Vercel integration Nx Cloud Manual Remote execution
Code generators and affected graph UI Limited Strong None Strong
Polyglot (Go, Java, Python) monorepo JS/TS focused Plugins exist JS only Best fit
Small repo (<5 packages) Optional; pnpm -r may suffice Overkill Simplest Overkill

Common pitfalls

  • Missing or wrong outputs — cache hits restore empty folders; builds pass in CI but deploy broken artifacts. Audit every task’s output globs.
  • Unlisted env vars — staging cache served to production when NEXT_PUBLIC_* URLs differ. Add all output-affecting vars to env.
  • Dev tasks cached accidentally — set cache: false on dev and watch scripts.
  • Running turbo outside repo rootturbo.json must live at the workspace root; nested copies confuse discovery.
  • Shallow git clone in CI--filter=...[origin/main] needs merge base history; use fetch-depth: 0.
  • Mixing package managers — lockfile hash changes invalidate everything; standardize on pnpm before enabling remote cache.
  • Secrets in cache keys — never list API keys in env; use passThroughEnv for runtime-only secrets.

Production checklist

  • Install turbo as root devDependency; pin major version in lockfile.
  • Define build, lint, test, and typecheck tasks with correct dependsOn and outputs.
  • Mark dev tasks cache: false and persistent: true.
  • List env vars that change compiled output in each task’s env array.
  • Enable remote cache: turbo link locally; set TURBO_TOKEN and TURBO_TEAM in CI.
  • CI uses --filter=...[origin/main] on pull requests; full build on main nightly.
  • Cache node_modules/.cache/turbo in CI as a fallback.
  • Document turbo run --dry-run for debugging task order.
  • Run --force after changing output globs or global dependencies.
  • Review cache hit ratio monthly; low hits often mean misconfigured inputs.

Key takeaways

  • Turborepo orchestrates monorepo scripts with a task DAG, not a new package manager.
  • turbo.json defines dependencies, cacheable outputs, and env-sensitive hashing.
  • Local + remote cache skips redundant lint, test, and build work across machines.
  • Filters limit CI to packages affected by a diff when paired with full git history.
  • Choose Nx or Bazel when you need generators, polyglot builds, or enterprise graph tooling beyond caching.

Related reading