Turborepo in Practice: Setup and Build Caching

Setting up Turborepo, watching it build in dependency order, and the cache hit that went from 5 seconds to 3 milliseconds.

March 21, 20263 min read3 / 6

The gap between "installed Turborepo" and "Turborepo doing useful things" is smaller than it seems. The configuration is minimal because Turborepo's value is in leveraging what you've already declared — your package.json dependencies and npm scripts — rather than duplicating that information.

Installation

Install Turborepo at the workspace root, not in any individual package:

Bash
pnpm add -D turbo -w

The -w flag installs at the workspace root. Turborepo is a dev dependency for the whole monorepo, not a dependency of any individual app or package.

The turbo.json

Drop this at the workspace root next to package.json:

JSON
{ "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "type-check": { "dependsOn": ["^build"] }, "lint": {}, "test": { "dependsOn": ["build"] }, "dev": { "persistent": true, "cache": false } } }

That's it. Turborepo reads this, reads your package.json workspace configuration, derives the dependency graph, and is ready.

What Happens When You Run a Build

Bash
npx turbo run build

Turborepo:

  1. Reads all package.json files across apps/ and packages/
  2. Builds the dependency graph from dependencies/devDependencies
  3. Identifies which packages have a build script
  4. Executes builds in dependency order, parallelizing where possible
  5. Hashes the output files in each dist/ directory
  6. Stores the cache locally (and remotely, if configured)

First run: 5.316 seconds — building everything from scratch.

Run it again immediately without changing anything: 2.93 milliseconds — pure cache hit. Turborepo hashed the outputs, compared to stored hashes, found no changes, and skipped every build.

The difference in CI, where you're running Playwright tests instead of just builds, is the difference between 17 minutes and 2 minutes. I've hit those numbers. I was surprised even knowing how the caching works.

Making Turborepo Invisible

Once it's working, wrap it in your existing npm scripts so nothing in the developer workflow changes:

JSON
{ "scripts": { "build": "turbo run build", "type-check": "turbo run type-check", "lint": "turbo run lint", "test": "turbo run test", "dev": "turbo run dev" } }

Now developers run pnpm build, pnpm test, pnpm dev — exactly as before. Turborepo is invisible. The caching and dependency ordering happen automatically.

The Key Behaviors to Understand

"dependsOn": ["^build"] — the ^ prefix means "run build in all my dependencies before running build in me." This is what enforces the correct build order automatically.

"outputs": ["dist/**"] — Turborepo hashes these files to determine cache validity. If nothing in dist/ changed, it's a cache hit. All packages should output to the same directory name (I use dist/) to keep this consistent.

"persistent": true on dev — the dev server doesn't exit, so Turborepo shouldn't wait for it to finish or cache its "output." This tells Turborepo to start it and move on.

"cache": false on dev — don't cache dev server runs. You want a fresh start every time.

One thing that surprised me when I first read through the Turborepo docs: there isn't actually that much more to it than this. The power comes from the caching and graph resolution, not from extensive configuration.

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.