Update Turborepo pipeline
Turborepo has been building your packages in the correct order (packages first, then apps), but you haven't explicitly configured this - it's been guessing based on workspace dependencies. As your monorepo grows, you need explicit task configuration to control build order, caching, and parallel execution.
You'll configure turbo.json to define task dependencies: "build apps only after building their package dependencies" and "lint apps only after linting packages." This gives you full control over task orchestration.
Outcome
Understand and configure Turborepo's task pipeline with explicit dependencies.
Fast track
- Review current turbo.json configuration
- Understand ^build and ^lint dependency syntax
- Visualize the dependency graph
- Test build and lint with dependency order
Hands-on exercise 4.3
Configure Turborepo task dependencies for optimal build orchestration.
Requirements:
- Review turbo.json task configuration
- Understand
dependsOn: ["^build"]syntax - Add build script to packages (ui, utils, config)
- Run
turbo buildand observe execution order - Run
turbo lintand see parallel execution - Understand when tasks run in parallel vs sequential
Implementation hints:
^buildmeans "dependencies' build tasks first"- Tasks without dependencies run in parallel
- Each package needs a build script in package.json
- Use --dry flag to see execution plan
Review current turbo.json
Open turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Let's understand what each field means:
build task:
"dependsOn": ["^build"]- Run build on all workspace dependencies first"outputs": [".next/**", ...]- Cache these directories after build
lint task:
"dependsOn": ["^lint"]- Lint dependencies before linting this package
dev task:
"cache": false"- Never cache dev (it's a watch mode)"persistent": true"- Keep running after completion
Understanding dependency syntax
The ^ prefix means "workspace dependencies":
{
"build": {
"dependsOn": ["^build"]
}
}Translation: "Before building this package, first run build on all packages it depends on"
Example flow for apps/snippet-manager:
apps/snippet-manager depends on:
- packages/ui
- packages/utils
Run: turbo build --filter=@geniusgarage/snippet-manager
Execution order:
1. Build packages/ui
2. Build packages/utils
3. Build apps/snippet-manager (after dependencies complete)
Without ^ prefix:
{
"build": {
"dependsOn": ["lint"] // No ^ prefix
}
}This means: "Before building, run lint in the same package"
Add build scripts to packages
Currently, only apps have build scripts. Add build to packages:
Packages/ui/package.json
{
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint ."
}
}TypeScript type-checking is the "build" for UI components.
Packages/utils/package.json
{
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint ."
}
}Same - just type-check the utilities.
Notice we're NOT adding build scripts to packages/typescript-config or packages/eslint-config. These packages export static JSON and JavaScript files - there's nothing to build or compile. They're pure configuration.
Only packages with source code that needs transformation (like TypeScript compilation) need build scripts.
Try it
1. See the execution plan (dry run)
turbo build --dryOutput:
Tasks to Run
@geniusgarage/utils:build
@geniusgarage/ui:build
@geniusgarage/web:build
@geniusgarage/snippet-manager:build
Notice the order:
- Packages first (utils, ui) - they have no dependencies
- Apps last (web, snippet-manager) - they depend on packages
Packages run in parallel (no dependencies on each other). Apps run after packages complete.
2. Run the actual build
turbo buildOutput:
@geniusgarage/utils:build: tsc --noEmit
@geniusgarage/ui:build: tsc --noEmit
✓ All package builds complete
@geniusgarage/web:build: next build
@geniusgarage/snippet-manager:build: next build
✓ All app builds complete
Tasks: 4 successful, 4 total
Cached: 0 cached, 4 total
Time: 11.234s
Execution flow:
- utils, ui build in parallel (type-checking only)
- Wait for all packages to complete
- web, snippet-manager build in parallel (full Next.js builds)
3. Run build again (see caching)
turbo buildOutput:
@geniusgarage/utils:build: cache hit, replaying outputs
@geniusgarage/ui:build: cache hit, replaying outputs
@geniusgarage/web:build: cache hit, replaying outputs
@geniusgarage/snippet-manager:build: cache hit, replaying outputs
Tasks: 4 successful, 4 total
Cached: 4 cached, 4 total
Time: 195ms ⚡
11s → 195ms because everything was cached!
4. Change a util and rebuild
Edit packages/utils/src/index.ts:
export function formatDate(date: Date): string {
// Add a comment to invalidate cache
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(date)
}Run build:
turbo buildOutput:
@geniusgarage/utils:build: cache miss, executing 1.123s
@geniusgarage/ui:build: cache hit
@geniusgarage/web:build: cache hit
@geniusgarage/snippet-manager:build: cache miss, executing 4.891s
Tasks: 4 successful, 4 total
Cached: 2 cached, 4 total
Time: 5.234s
Smart caching:
- ui, web cached (unchanged)
- utils rebuilt (source changed)
- snippet-manager rebuilt (depends on utils which changed)
Turborepo detected that apps/snippet-manager depends on packages/utils, so it rebuilt the app even though the app's code didn't change!
How Turborepo orchestrates tasks
Your monorepo has this dependency structure:
apps/web depends on:
└─ packages/ui
└─ packages/typescript-config (devDependency - no build)
└─ packages/eslint-config (devDependency - no build)
apps/snippet-manager depends on:
└─ packages/ui
└─ packages/utils
└─ packages/typescript-config (devDependency - no build)
└─ packages/eslint-config (devDependency - no build)
packages/utils depends on:
└─ packages/typescript-config (devDependency - no build)
└─ packages/eslint-config (devDependency - no build)
packages/ui depends on:
└─ packages/typescript-config (devDependency - no build)
└─ packages/eslint-config (devDependency - no build)
Config packages (typescript-config, eslint-config) are devDependencies that apps use at build time, but they don't have their own build tasks. They export static files, so Turborepo doesn't need to orchestrate them.
Only packages with build scripts appear in the execution graph.
When you run turbo build:
-
Level 1 (parallel):
- packages/utils (no build dependencies)
- packages/ui (no build dependencies)
-
Level 2 (parallel, after Level 1):
- apps/web (depends on ui)
- apps/snippet-manager (depends on ui, utils)
Turborepo automatically figures out this order based on workspace dependencies and ^build configuration!
Common turbo.json patterns
Run tests before build
{
"build": {
"dependsOn": ["test", "^build"],
"outputs": [".next/**"]
}
}This runs tests in the same package, then builds dependencies, then builds the package.
Cache-only tasks
{
"test": {
"cache": true,
"outputs": ["coverage/**"]
}
}Tests are cached. Re-run only when source code changes.
Never cache
{
"deploy": {
"cache": false
}
}Deploy tasks should never be cached.
Commit
No code changes needed - turbo.json was already configured correctly. But let's add build scripts:
git add .
git commit -m "chore: add build scripts to all packages"Done-when
Verify Turborepo pipeline works:
- Reviewed turbo.json configuration
- Understood
^buildsyntax means "dependencies first" - Understood
^lintsyntax for lint dependencies - Added build script to
packages/ui(tsc --noEmit) - Added build script to
packages/utils(tsc --noEmit) - Understood why config packages don't need build scripts (static files)
- Ran
turbo build --dryand saw execution plan - Ran
turbo buildand saw packages build first, then apps - Ran
turbo buildagain and saw full cache hit - Changed packages/utils and saw selective rebuild
- Saw apps/snippet-manager rebuild (depends on changed utils)
- Understood how Turborepo determines task execution order
- Understood parallel execution (packages) vs sequential (dependencies)
What's Next
Section 3 complete! You have:
- 3 shared packages (ui, config, utils)
- Centralized configuration
- Explicit task dependencies
- Smart caching and orchestration
Section 4: Testing - Add Vitest to packages/ui, write component tests, configure Turbo for tests, and see test caching in action. You'll prove that testing works seamlessly in monorepos with proper task orchestration.
Was this helpful?