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
buildscript. - Reads
turbo.jsonto 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—^buildmeans “runbuildin 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:
pricing-core#buildandui-kit#build(no upstream deps).dashboard#buildafter 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— emitsdist/; no upstream tasks.packages/ui-kit#build— depends on^build; Storybook static export tostorybook-static/.apps/dashboard#build— Vite production bundle; depends on both libraries.apps/pricing-api#build— TypeScript compile only; depends onpricing-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
| Need | Turborepo | Nx | pnpm -r only | Bazel |
|---|---|---|---|---|
| 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 toenv. - Dev tasks cached accidentally — set
cache: falseondevand watch scripts. - Running turbo outside repo root —
turbo.jsonmust live at the workspace root; nested copies confuse discovery. - Shallow git clone in CI —
--filter=...[origin/main]needs merge base history; usefetch-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; usepassThroughEnvfor runtime-only secrets.
Production checklist
- Install
turboas root devDependency; pin major version in lockfile. - Define
build,lint,test, andtypechecktasks with correctdependsOnandoutputs. - Mark
devtaskscache: falseandpersistent: true. - List env vars that change compiled output in each task’s
envarray. - Enable remote cache:
turbo linklocally; setTURBO_TOKENandTURBO_TEAMin CI. - CI uses
--filter=...[origin/main]on pull requests; full build on main nightly. - Cache
node_modules/.cache/turboin CI as a fallback. - Document
turbo run --dry-runfor debugging task order. - Run
--forceafter 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.jsondefines 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
- Monorepo architecture explained — boundaries, versioning, and when to adopt Turborepo
- pnpm fundamentals explained — workspaces and lockfiles Turborepo orchestrates
- GitHub Actions fundamentals explained — CI wiring for remote cache tokens
- Vite fundamentals explained — typical
buildscript inside cached app packages