/ 3 min read

Taming 60+ Packages with Turborepo


This is Part 3 of a series on plan-based development with Claude Code. Today we explore how Turborepo makes our massive monorepo manageable.


Why Build Orchestration Matters


In a monorepo with 60+ packages, the naive approach to building and testing is brutally slow. Run every test? That’s 20 minutes. Build every package? Another 15. Type-check everything? Add another 10.


Multiply these numbers by the frequency of commits, and you have a recipe for slow iteration cycles and frustrated developers.


Turborepo solves this with intelligent caching and parallel execution. But the real power comes from how you configure it.


Our Turborepo Configuration Philosophy


Our turbo.json evolved through many iterations. Here’s the philosophy behind our current setup:


1. Declare Dependencies Explicitly


{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"check-types": {
"dependsOn": ["build", "^build"],
"inputs": ["$TURBO_DEFAULT$", "tsconfig.json"]
},
"dev": {
"dependsOn": ["^build"],
"persistent": true,
"interruptible": true
}
}
}

The ^build syntax means “build my dependencies first.” This ensures packages are built in the correct order without manual coordination.


2. Precise Input Definitions


{
"build": {
"inputs": [
"$TURBO_DEFAULT$",
".env*",
"!**/*.test.{ts,tsx}",
"!**/*.spec.{ts,tsx}",
"!coverage/**",
"!storybook-static/**"
]
}
}

By excluding test files and coverage reports from build inputs, we avoid rebuilding packages when only tests change. This seemingly small optimization saves significant time across the full repository.


3. Package Boundaries


One of Turborepo’s powerful features is boundary enforcement:


{
"boundaries": {
"tags": {
"db": {
"dependencies": {
"deny": ["app", "ui", "ai"]
}
},
"api": {
"dependencies": {
"deny": ["app", "ui", "ai"]
}
},
"ui": {
"dependencies": {
"deny": ["app"]
}
}
}
}
}

This configuration enforces architectural rules:

  • Database packages can’t depend on UI or AI packages
  • API packages are pure logic, no UI dependencies
  • UI packages don’t directly depend on apps

When someone accidentally imports a UI component into a database package, turbo boundaries catches it immediately.


Real Performance Numbers


Before and after Turborepo optimization:


OperationBeforeAfterImprovement
Full build (cold)4m 30s4m 30s0% (same)
Full build (cached)4m 30s8s97%
Type check (cached)3m 15s12s94%
Test (affected)8m45s91%
Dev startup90s15s83%

The key insight: Turborepo’s value isn’t in the first run—it’s in every subsequent run. Most of your development time is incremental, not from-scratch.


Package-Level Configuration


Each package has its own turbo.json that extends the root:


packages/db/turbo.json
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tags": ["db"],
"tasks": {
"build": {
"env": ["DATABASE_URL", "DB_*", "DATABASE_MODE"],
"outputs": ["dist/**"]
}
}
}

This allows packages to declare their specific environment variable dependencies. When DATABASE_URL changes, only packages that depend on it rebuild.


The Feedback Loop Impact


Fast builds enable a different kind of development:


  1. Experiment freely: Quick validation means you try more ideas
  2. Catch errors early: Run bun run check-types after every significant change
  3. Stay in flow: No 10-minute builds breaking your concentration
  4. Review faster: CI completes in minutes, not hours

The mantra we’ve adopted: If verification takes less than 30 seconds, you’ll do it constantly. If it takes more than 5 minutes, you’ll skip it.


Turborepo puts most verifications in that under-30-second category.


Next up in Part 4: TypeScript strict mode as your first line of defense.