Update Turborepo pipeline
So far, turbo build has only ever run two tasks. packages/ui and packages/utils ship TypeScript source with no build script, and each app's bundler compiles them on the way through. That keeps setup light, but nothing checks a package's types until an app happens to import the broken file. As your monorepo grows, you need explicit task configuration to control build order, caching, and parallel execution.
We'll give the packages a check-types task, wire it into turbo.json, and look at how dependsOn decides what runs when. 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 a
check-typesscript to packages (ui, utils) - Add a
check-typestask to turbo.json, then runturbo check-typesandturbo 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
- Packages that ship TypeScript source get
check-types, notbuild(Just-in-Time Packages) - Use --dry flag to see execution plan
Review current turbo.json
Open turbo.json:
{
"$schema": "https://turborepo.dev/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. Look for build in packages/ui and packages/utils (neither has one, so nothing runs)
2. Build apps/snippet-manager
The packages still count. Turborepo keeps them in the graph as transit nodes, so a change in packages/utils changes snippet-manager's hash even though utils never runs a build.
Without ^ prefix:
{
"build": {
"dependsOn": ["lint"] // No ^ prefix
}
}This means: "Before building, run lint in the same package"
Add check-types scripts to packages
Our first instinct might be to give each package a build script that runs tsc --noEmit. That works, but it puts a do-nothing step in front of every app build. packages/ui ships source that each app's bundler compiles, so there's nothing for it to build. What it needs is a type-check, and the Turborepo docs call that task check-types.
Packages/ui/package.json
{
"scripts": {
"check-types": "tsc --noEmit",
"lint": "eslint ."
}
}Packages/utils/package.json
{
"scripts": {
"check-types": "tsc --noEmit",
"lint": "eslint ."
}
}Same script. tsc --noEmit reads each package's tsconfig.json and reports type errors without writing any files.
Add the task to turbo.json
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}^check-types means a package type-checks after the packages it depends on. There are no outputs, because type-checking writes nothing. Turborepo still caches the logs, so an unchanged package replays its result.
Try it
1. See the execution plan (dry run)
turbo check-types --dryOutput:
Tasks to Run
@geniusgarage/ui:check-types
@geniusgarage/utils:check-types
Neither package depends on the other, so ^check-types has nothing to wait for. Both run in parallel.
2. Type-check, then build
turbo check-typesOutput:
@geniusgarage/ui:check-types: cache miss, executing 1.402s
@geniusgarage/utils:check-types: cache miss, executing 1.123s
Tasks: 2 successful, 2 total
Cached: 0 cached, 2 total
Time: 1.534s
A new tsconfig.tsbuildinfo file shows up in packages/utils. That's expected: the shared base.json turns on incremental, so tsc saves its progress there to make the next check faster. The starter's .gitignore already skips these files.
Now build:
turbo buildOutput:
@geniusgarage/web:build: cache miss, executing 5.123s
@geniusgarage/snippet-manager:build: cache miss, executing 4.891s
Tasks: 2 successful, 2 total
Cached: 0 cached, 2 total
Time: 5.234s
Execution flow:
turbo check-typesruns ui and utils in parallel (type-checking only)turbo buildruns web and snippet-manager in parallel (full Next.js builds)- No package task runs during
turbo build
Both apps missed the cache, and that's expected. We edited package.json in packages/ui and packages/utils, and those files feed into the apps' hashes.
3. Run build again (see caching)
turbo buildOutput:
@geniusgarage/web:build: cache hit, replaying logs
@geniusgarage/snippet-manager:build: cache hit, replaying logs
Tasks: 2 successful, 2 total
Cached: 2 cached, 2 total
Time: 195ms >>> FULL TURBO
5s → 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/web:build: cache hit, replaying logs
@geniusgarage/snippet-manager:build: cache miss, executing 4.891s
Tasks: 2 successful, 2 total
Cached: 1 cached, 2 total
Time: 4.987s
Smart caching:
- web cached (doesn't depend on utils)
- 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! packages/utils ran nothing during the build, but its files are still part of snippet-manager's hash. Run turbo check-types now and you'll see utils miss while ui replays from cache.
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)
When you run turbo build:
- apps/web and apps/snippet-manager build in parallel
- Each package they depend on runs nothing, but its hash feeds into theirs
When you run turbo check-types:
- packages/ui and packages/utils type-check in parallel
- If one of them depended on the other,
^check-typeswould make it wait for that package's check first
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
Commit the new scripts and the check-types task:
git add .
git commit -m "chore: add check-types to ui and utils"Done-when
Verify Turborepo pipeline works:
- Reviewed turbo.json configuration
- Understood
^buildsyntax means "dependencies first" - Understood
^lintsyntax for lint dependencies - Added
check-typesscript topackages/ui(tsc --noEmit) - Added
check-typesscript topackages/utils(tsc --noEmit) - Added a
check-typestask to turbo.json with^check-types - Understood why config packages don't need
check-types(static files) - Ran
turbo check-types --dryand saw both packages in the plan - Ran
turbo check-typesand saw ui and utils type-check in parallel - Ran
turbo buildand saw only the two apps build - 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?