Learn-skills.dev turborepo
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/acedergren/agentic-tools/turborepo" ~/.claude/skills/neversight-learn-skills-dev-turborepo-c4dff2 && rm -rf "$T"
data/skills-md/acedergren/agentic-tools/turborepo/SKILL.mdTurborepo - Monorepo Architecture Expert
Assumption: You know
turbo run build. This covers architectural decisions.
Before Adopting Turborepo: Strategic Assessment
Ask yourself these questions BEFORE committing to monorepo:
1. Team & Coordination Analysis
- Team size: 1-3 engineers → Polyrepo simpler (monorepo overhead not worth it)
- Shared code percentage: <20% → Polyrepo, >50% → Monorepo compelling
- Coordination pain: Breaking changes require 3+ repos updated → Monorepo wins
- Deployment coupling: Services deploy together → Monorepo, Independently → Polyrepo
2. Technical Complexity Assessment
- Languages: Pure JS/TS → Turborepo works, Mixed (Go/Python) → Nx or polyrepo
- Build time: <5min total across all apps → Overhead not justified yet
- Cache importance: Long builds (>2min per package) → Turborepo caching critical
- CI complexity: Simple pipeline → Polyrepo easier, Complex (affected detection) → Monorepo
3. Maintenance Cost Analysis
- What breaks with monorepo: Version conflicts, build order issues, cache debugging, tooling complexity
- What breaks with polyrepo: API version hell, coordination overhead, code duplication, cross-repo changes
- Break-even point: Monorepo worth it when 3+ apps share 30%+ code + frequent coordination needed
Critical Rule: Package Tasks, Not Root Tasks
The #1 Turborepo mistake: Putting task logic in root
package.json.
// ❌ WRONG - defeats parallelization // Root package.json { "scripts": { "build": "cd apps/web && next build && cd ../api && tsc", "lint": "eslint apps/ packages/", "test": "vitest" } } // ✅ CORRECT - parallel execution // apps/web/package.json { "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } } // apps/api/package.json { "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } // Root package.json - ONLY delegates { "scripts": { "build": "turbo run build" } }
Why it breaks: Turborepo can't parallelize sequential shell commands. Package tasks enable task graph parallelization.
Decision: When to Split a Package
Considering splitting code into package? │ ├─ Code used by 1 app only → DON'T split yet │ └─ Keep in app until second consumer appears │ WHY: Premature abstraction, overhead > benefit │ ├─ Code used by 2+ apps → MAYBE split │ ├─ Stable API (rarely changes) → Split │ ├─ Unstable (changes every sprint) → DON'T split yet │ └─ Mixed team ownership → DON'T split (use import path instead) │ WHY: Shared packages need stable APIs + clear owners │ ├─ Publishing to npm → MUST split │ └─ External packages require independent versioning │ └─ CI builds too slow (> 10min) → Split strategically └─ Split by stability (core vs features), not by domain WHY: Stable packages cache, unstable packages rebuild
Anti-pattern: Creating packages for "clean architecture" without consumers. Packages add overhead (build, test, version).
Anti-Patterns
❌ #1: Circular Dependencies
Problem: Packages depend on each other, breaks task graph
packages/ui → imports from packages/utils packages/utils → imports from packages/ui // ❌ Circular
Detection:
turbo run build # Fails with: "Could not resolve dependency graph"
Fix: Extract shared code to third package
packages/ui → packages/shared packages/utils → packages/shared
Why it breaks: Turborepo builds dependencies first (topological sort). Circular deps = no valid build order.
Why this is deceptively hard to debug: Error message "Could not resolve dependency graph" doesn't mention the word "circular"—just lists package names. With 10+ packages, takes 15-20 minutes to manually trace imports and realize two packages reference each other. The import chain might be indirect (A → B → C → A), making it even harder to spot. Developers waste time checking turbo.json config and workspace setup before realizing it's an import cycle issue, not a Turborepo configuration problem.
❌ #2: Overly Granular Packages
Problem: 50 micro-packages, every import crosses package boundary
packages/button/ packages/input/ packages/checkbox/ packages/radio/ packages/select/ // ... 45 more single-component packages
Symptoms:
- Every change touches 5+ packages
- 10+ version bumps per feature
version hellpnpm workspace:*
Fix: Group by stability/purpose
packages/ui/ # All components (changes often) packages/ui-primitives/ # Headless components (stable) packages/icons/ # Generated SVGs (rarely changes)
Decision rule: Package boundary = different change frequency
Why this is deceptively hard to debug: Takes weeks or months to discover the problem—not immediate. First feature seems fine (update 3 packages, publish 3 versions). Second feature touches 5 packages. Third feature hits 10 packages and you're managing workspace version conflicts for 2 hours. The pain accumulates slowly: CI gets slower (building 50 packages), version bumps become tedious (changesets for 10+ packages), developers avoid refactoring because it crosses too many boundaries. Only after 3-6 months do you realize the granularity was wrong, but by then you have 50 packages and merging them requires major migration work.
❌ #3: Missing Task Dependencies
Problem: Tests run before build completes
// turbo.json { "tasks": { "build": { "outputs": ["dist/**"] }, "test": {} // ❌ No dependsOn } } // Result: tests import from dist/ before it exists
Fix: Explicit dependencies
{ "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "test": { "dependsOn": ["build"] } // ✅ Build first } }
Why:
^build = build dependencies first. build = build this package first.
Why this is deceptively hard to debug: Tests pass locally (you ran
build manually first), fail in CI with cryptic errors like "Cannot find module './dist/index.js'" or import errors. The race condition is timing-dependent—sometimes tests start before build finishes, sometimes they start after (especially with caching). Developers waste 10-15 minutes checking import paths, package.json exports, and tsconfig before realizing it's a task ordering issue. The error message points to the symptom (missing file) not the cause (missing dependency declaration).
❌ #4: Cache Miss Hell
Problem: Cache never hits, rebuilds everything
// turbo.json { "tasks": { "build": { "inputs": ["src/**"] // ❌ Too broad } } } // Any file change (even comments) = cache miss
Fix: Exclude non-code files
{ "tasks": { "build": { "inputs": [ "src/**/*.{ts,tsx}", // ✅ Only source files "!src/**/*.test.ts" // Exclude tests ] } } }
Debug cache:
turbo run build --dry --graph # Shows why cache missed
Why this is deceptively hard to debug: Cache initially works (first few builds hit), then mysteriously stops. Every run shows "cache miss" but you can't tell why. The problem: you added a README.md to src/, touched a comment, or updated a test file—non-code changes that shouldn't trigger rebuild but do because inputs include
src/**. Developers waste 20-30 minutes checking remote cache credentials, clearing local cache, restarting daemon before realizing the input glob is too broad. The --dry --graph flag shows hash changed but doesn't clearly indicate WHICH file caused it (need to diff file lists manually).
Decision: Monorepo vs Polyrepo
Starting new project? │ ├─ Single team, single product → Polyrepo (simpler) │ └─ One repo per service/app │ WHY: Monorepo overhead not worth it for small teams │ ├─ Shared UI library → Monorepo │ └─ Library + consumer apps in same repo │ WHY: Develop library + test in consumers simultaneously │ ├─ Microservices (different languages) → Polyrepo │ └─ Go service, Python service, Node service │ WHY: Turborepo is JS/TS focused, polyrepo simpler │ └─ Multiple teams, shared code → Monorepo └─ Need atomic changes across boundaries WHY: One PR changes API + all consumers
Real-world: Most projects should start polyrepo, migrate to monorepo when pain > tooling cost.
Package Boundary Patterns
Pattern 1: By Stability
packages/ core/ # Changes quarterly (semantic versioning) features/ # Changes weekly (workspace protocol) utils/ # Changes monthly
Benefit: Stable packages cache longer, ship to npm independently.
Pattern 2: By Consumer
packages/ public-api/ # External consumers internal/ # Internal apps only
Benefit: Clear API surface, different versioning strategies.
Pattern 3: By Team
packages/ team-platform/ team-growth/ team-infra/
Warning: Only works if teams rarely share code. Otherwise creates silos.
Turborepo vs Alternatives
Choose Turborepo when: ✅ JS/TS monorepo (React, Next.js, Node) ✅ Need remote caching (Vercel, self-hosted) ✅ Task graph parallelization important ✅ Using pnpm workspaces or npm workspaces Choose Nx when: ✅ Need project graph visualization ✅ Polyglot (JS + Python + Go) ✅ Want opinionated project structure ✅ Need plugin ecosystem Choose Rush when: ✅ Very large monorepo (100+ packages) ✅ Need phantom dependencies detection ✅ Publishing to npm is primary use case
Real-world: Turborepo wins for Next.js/React apps, Nx wins for complex polyglot, Rush wins for library publishers.
Debugging Commands
Visualize task graph
turbo run build --dry --graph=graph.html # Opens browser with task dependency visualization
Find cache misses
turbo run build --dry=json | jq '.tasks[] | select(.cache.status == "MISS")'
Check package dependency order
turbo run build --dry --graph | grep "→"
Test cache without running tasks
turbo run build --dry # Shows what would run
Error Recovery Procedures
When Cache Never Hits (Cache Miss Hell)
Recovery steps:
- Diagnose: Run
to see current hashturbo run build --dry=json | jq '.tasks[0].hash' - Identify culprit: Add
to see which files changed the hash--log-order=grouped - Fix inputs: Narrow glob patterns to exclude non-code files (tests, docs, configs)
- Fallback: If still missing, disable cache for that task temporarily:
in turbo.json, then debug without cache pressure"cache": false
When Circular Dependency Error Occurs
Recovery steps:
- Visualize: Run
and open in browserturbo run build --dry --graph=graph.html - Trace cycle: Look for packages that appear in each other's dependency chains (A → B → ... → A)
- Extract shared: Create new package (e.g.,
) and move common code therepackages/shared - Fallback: If cycle is complex (3+ packages), use dependency graph tool like
to visualize:madgenpx madge --circular --extensions ts,tsx packages/
When Tests Fail in CI But Pass Locally
Recovery steps:
- Check task order: Run
to see if build runs before testturbo run test --dry --graph - Add dependencies: Add
to test task in turbo.json"dependsOn": ["build"] - Verify: Run
(bypass cache) to confirm tests pass when build runs firstturbo run test --force - Fallback: If still failing, check for race condition in parallel tests: add
to test task temporarily and see if issue persists"cache": false
When Overly Granular Packages Cause Version Hell
Recovery steps:
- Audit changes: Run
to count package version bumpsgit log --oneline --since="1 month ago" -- packages/ - Identify clusters: Look for packages that always change together (5+ times in last month)
- Merge packages: Combine related packages into single package with internal structure
- Fallback: If merging is too risky, use
protocol to auto-link versions and reduce manual bumpsworkspace:*
When to Load Full Reference
MANDATORY - READ ENTIRE FILE:
references/cli-options.md when:
- Encountering 3+ unknown CLI flags in error messages or commands
- Need advanced filtering across 10+ packages (--filter patterns, --affected usage)
- Setting up 5+ complex task pipeline options (--concurrency, --continue, --output-logs)
- Troubleshooting CLI behavior that's not covered in this core framework
MANDATORY - READ ENTIRE FILE:
references/remote-cache-setup.md when:
- Setting up remote cache for team with 3+ developers
- Debugging 5+ cache authentication or connection errors
- Configuring self-hosted remote cache with custom storage backend
- Implementing cache security policies (signature verification, access control)
Do NOT load references for:
- Basic architecture decisions (use this core framework)
- Single cache miss debugging (use Error Recovery section above)
- Deciding whether to adopt monorepo (use Strategic Assessment section)
Resources
- Official Docs: https://turbo.build/repo/docs (for CLI reference)
- This Skill: Architecture decisions, anti-patterns, package boundaries