Vercel Logo

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

  1. Review current turbo.json configuration
  2. Understand ^build and ^lint dependency syntax
  3. Visualize the dependency graph
  4. Test build and lint with dependency order

Hands-on exercise 4.3

Configure Turborepo task dependencies for optimal build orchestration.

Requirements:

  1. Review turbo.json task configuration
  2. Understand dependsOn: ["^build"] syntax
  3. Add a check-types script to packages (ui, utils)
  4. Add a check-types task to turbo.json, then run turbo check-types and turbo build and observe execution order
  5. Run turbo lint and see parallel execution
  6. Understand when tasks run in parallel vs sequential

Implementation hints:

  • ^build means "dependencies' build tasks first"
  • Tasks without dependencies run in parallel
  • Packages that ship TypeScript source get check-types, not build (Just-in-Time Packages)
  • Use --dry flag to see execution plan

Review current turbo.json

Open turbo.json:

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

packages/ui/package.json
{
  "scripts": {
    "check-types": "tsc --noEmit",
    "lint": "eslint ."
  }
}

Packages/utils/package.json

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

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.

Config Packages Don't Need check-types

Notice we're NOT adding check-types to packages/typescript-config or packages/eslint-config. These packages export static JSON and JavaScript files. They're pure configuration.

Only packages with TypeScript source need check-types.

Try it

1. See the execution plan (dry run)

turbo check-types --dry

Output:

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-types

Output:

@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 build

Output:

@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:

  1. turbo check-types runs ui and utils in parallel (type-checking only)
  2. turbo build runs web and snippet-manager in parallel (full Next.js builds)
  3. 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 build

Output:

@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:

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 build

Output:

@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)
Packages Without a Build Script

None of the packages have a build script, so none of them run anything during turbo build. They stay in the graph as transit nodes anyway. When an app depends on a package, including a config package like typescript-config, that package's files are part of the app's build hash.

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-types would 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 ^build syntax means "dependencies first"
  • Understood ^lint syntax for lint dependencies
  • Added check-types script to packages/ui (tsc --noEmit)
  • Added check-types script to packages/utils (tsc --noEmit)
  • Added a check-types task to turbo.json with ^check-types
  • Understood why config packages don't need check-types (static files)
  • Ran turbo check-types --dry and saw both packages in the plan
  • Ran turbo check-types and saw ui and utils type-check in parallel
  • Ran turbo build and saw only the two apps build
  • Ran turbo build again 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?

supported.