Turborepo
Turborepo (turbo) is a high-performance build system for JavaScript/TypeScript monorepos. It does not replace Vite, Webpack, or tsup — it orchestrates their scripts with a dependency-aware pipeline, content-aware caching, and optional remote cache sharing across CI agents and laptops.
If you already use pnpm/npm/yarn workspaces and find yourself re-building unchanged packages on every PR, Turborepo is the usual next step. Reach for Nx when you also want generators, module boundaries, and a richer project graph UI.
info
| 1 | # Root of a pnpm workspace monorepo |
| 2 | pnpm add -Dw turbo |
| 3 | |
| 4 | # Typical layout |
| 5 | # apps/web → Vite or Next app |
| 6 | # apps/api → Node service |
| 7 | # packages/ui → tsup library |
| 8 | # packages/config → shared eslint/tsconfig |
| 9 | # turbo.json |
| 10 | # pnpm-workspace.yaml |
| 1 | packages: |
| 2 | - "apps/*" |
| 3 | - "packages/*" |
Declare tasks in turbo.json. dependsOn: ["^build"]means "build dependencies first" (the caret targets upstream packages).
| 1 | { |
| 2 | "$schema": "https://turbo.build/schema.json", |
| 3 | "tasks": { |
| 4 | "build": { |
| 5 | "dependsOn": ["^build"], |
| 6 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"], |
| 7 | "env": ["NODE_ENV", "NEXT_PUBLIC_*"] |
| 8 | }, |
| 9 | "test": { |
| 10 | "dependsOn": ["build"], |
| 11 | "outputs": ["coverage/**"] |
| 12 | }, |
| 13 | "lint": { |
| 14 | "dependsOn": ["^build"] |
| 15 | }, |
| 16 | "dev": { |
| 17 | "cache": false, |
| 18 | "persistent": true |
| 19 | }, |
| 20 | "typecheck": { |
| 21 | "dependsOn": ["^build"], |
| 22 | "outputs": [] |
| 23 | } |
| 24 | } |
| 25 | } |
| Field | Meaning | When to set |
|---|---|---|
| dependsOn | Task graph edges | Always for build/test ordering |
| outputs | Artifacts restored from cache | Any task that writes files you reuse |
| env / passThroughEnv | Env vars that affect outputs | Prevent false cache hits |
| cache: false | Never cache | dev servers, deploy with side effects |
| persistent | Long-running process | dev / watch tasks |
warning
Turbo hashes inputs (files, deps, env, task definition). Matching hash → restore outputs and skip work. Local cache lives under .turbo.
| 1 | # Run build across the workspace |
| 2 | npx turbo run build |
| 3 | |
| 4 | # Only one package and its dependency graph |
| 5 | npx turbo run build --filter=@acme/web... |
| 6 | |
| 7 | # Dry insight into what would run |
| 8 | npx turbo run build --dry=json |
| 9 | |
| 10 | # Force ignore cache (debug) |
| 11 | npx turbo run build --force |
note
Remote cache shares artifacts between CI jobs and developers. Vercel Remote Cache is the common hosted option; self-hosted / S3-compatible solutions exist via community or enterprise setups.
| 1 | # Link to Vercel remote cache (interactive) |
| 2 | npx turbo login |
| 3 | npx turbo link |
| 4 | |
| 5 | # In CI, set token env vars (names per current Turbo docs) |
| 6 | # TURBO_TOKEN=... |
| 7 | # TURBO_TEAM=... |
| 8 | |
| 9 | npx turbo run build lint test --concurrency=4 |
| Benefit | How | When it pays off |
|---|---|---|
| PR CI minutes drop | Reuse main's build artifacts | Many packages, frequent PRs |
| Laptop parity | Devs pull remote hits | Large apps with long builds |
| Cold runners | Empty disk still hits remote | Ephemeral GitHub Actions VMs |
danger
Filters select packages by name, directory, or git change set — critical for large repos on CI.
| 1 | # Single package |
| 2 | turbo run build --filter=@acme/ui |
| 3 | |
| 4 | # Package + dependencies |
| 5 | turbo run build --filter=@acme/web... |
| 6 | |
| 7 | # Package + dependents |
| 8 | turbo run build --filter=...@acme/ui |
| 9 | |
| 10 | # Changed since main (pattern varies by turbo version) |
| 11 | turbo run build --filter=...[origin/main] |
| 12 | |
| 13 | # Exclude |
| 14 | turbo run build --filter=!@acme/docs |
Turbo invokes the same script name in each package. Standardize names: build, dev, lint, test, typecheck.
| 1 | { |
| 2 | "name": "@acme/ui", |
| 3 | "scripts": { |
| 4 | "build": "tsup", |
| 5 | "dev": "tsup --watch", |
| 6 | "lint": "eslint .", |
| 7 | "typecheck": "tsc --noEmit", |
| 8 | "test": "vitest run" |
| 9 | } |
| 10 | } |
| 1 | name: CI |
| 2 | on: |
| 3 | push: |
| 4 | branches: [main] |
| 5 | pull_request: |
| 6 | |
| 7 | jobs: |
| 8 | build: |
| 9 | runs-on: ubuntu-latest |
| 10 | steps: |
| 11 | - uses: actions/checkout@v4 |
| 12 | with: |
| 13 | fetch-depth: 0 |
| 14 | - uses: pnpm/action-setup@v4 |
| 15 | - uses: actions/setup-node@v4 |
| 16 | with: |
| 17 | node-version: 22 |
| 18 | cache: pnpm |
| 19 | - run: pnpm install --frozen-lockfile |
| 20 | - name: Turbo build |
| 21 | env: |
| 22 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} |
| 23 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} |
| 24 | run: pnpm turbo run build lint test --concurrency=2 |
best practice
| Aspect | Turborepo | Nx |
|---|---|---|
| Core job | Pipeline + cache | Dev toolkit + graph + cache |
| Config weight | Light (turbo.json) | Heavier (project.json / nx.json) |
| Generators | Minimal | First-class |
| Module boundaries | Convention / ESLint DIY | Built-in enforcement |
| Best fit | Existing workspaces needing speed | Large orgs wanting structure |
pro tip
- Forgetting ^build so apps compile against stale dependency sources.
- Caching dev tasks (always disable).
- Omitting env vars that change NEXT_PUBLIC_* embeds.
- Output globs that miss nested artifacts → empty restores.
- Running turbo without a lockfile-based install cache (still slow installs).
| Signal | Action |
|---|---|
| 2–3 packages, scripts already fine | pnpm -r may be enough |
| CI rebuilds everything every PR | Add Turborepo + remote cache |
| Need generators / boundaries | Prefer Nx (or Nx + turbo-like caching) |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.