TL;DR: Turborepo is Vercel's build system for monorepos — projects where multiple apps or packages live in one repository. Think of it as a general contractor who coordinates a construction crew: instead of each worker starting tasks randomly, the general contractor figures out the right order, runs jobs in parallel where possible, and remembers which rooms were already finished so the crew does not repaint them. In code terms: it builds your packages in the right order, skips builds that have not changed, and runs tasks in parallel to save time.
Why AI Coders Need to Know This
When you are just starting out as a vibe coder, your projects are simple: one folder, one app, one package.json. You run npm run dev and the whole thing starts up. Clean, simple, no surprises.
But as soon as you ask AI to build something with a little more structure — a frontend and a backend, or an app that shares components with another app — the AI often does something you did not expect: it creates a monorepo. One repository with multiple packages inside it. And when it does, it almost always reaches for Turborepo to manage it.
Suddenly your project looks like this:
my-app/
├── apps/
│ ├── web/ ← your Next.js frontend
│ └── api/ ← your Express backend
├── packages/
│ └── shared/ ← shared TypeScript types and utilities
├── turbo.json ← what is this??
├── package.json
└── pnpm-workspace.yaml
And the AI tells you to run turbo run build or turbo run dev instead of the npm run dev command you know. If you do not know what Turborepo is, you cannot debug it when it breaks, you cannot explain what it is doing, and you cannot make intelligent decisions about your project structure.
This guide fixes that. By the end, you will know exactly what every Turborepo-related file does, why AI generates it, and what to do when things go sideways.
The Problem Turborepo Solves
To understand Turborepo, you first need to understand the problem it was built to solve. Imagine you are renovating a house with three separate crews — a plumbing crew, an electrical crew, and a painting crew.
The naive approach: line them up and have each crew work one after the other. Plumbers finish, electricians start, electricians finish, painters start. Fine in theory, but it takes three times as long as it needs to, and if the painters already finished a bathroom last week and nothing changed in there, you are making them repaint it anyway — pure wasted effort.
The smart approach: a general contractor looks at the dependencies. Electricians cannot work in a room until the plumbers are done. But the painting crew can start on rooms where both are already finished. And if a room was painted last week and nothing changed, skip it entirely — it is already done. This is exactly what Turborepo does for code.
In a monorepo without Turborepo, every time you build, the system rebuilds everything from scratch — even packages that have not changed at all. In a large project, this can mean waiting 5–10 minutes for builds that should take 30 seconds. Turborepo solves this with two core ideas:
- Caching: If a package's source files have not changed since the last build, Turborepo skips the build and reuses the output from last time.
- Parallelism: Tasks that do not depend on each other run at the same time, on different CPU cores, instead of waiting in a queue.
Together, these two things can turn a 10-minute build into a 45-second build on first run — and a 0-second build on subsequent runs if nothing changed.
Real Scenario: AI Scaffolds a Monorepo
Here is exactly what happens when AI reaches for Turborepo. You type something like this:
Prompt I Would Type
Build me a full-stack SaaS starter with:
- A Next.js frontend
- An Express API backend
- Shared TypeScript types used by both
- Tell me the exact project structure and commands to run
The AI generates a monorepo structure with Turborepo and hands you several new files you have never seen before. Let us walk through each one.
What the AI Actually Generated: File by File
Here is the complete set of files AI typically creates when it scaffolds a Turborepo monorepo, with each one explained in plain language.
The root package.json
{
"name": "my-saas-app",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^2.0.0"
},
"workspaces": [
"apps/*",
"packages/*"
]
}
This is the root package.json — the master control panel for the whole monorepo. A few things are happening here that you have probably not seen before.
The "workspaces" field is telling your package manager: "There are multiple packages in this repo. Look inside the apps/ and packages/ folders — each subfolder there is its own separate package." Without this, the package manager would not know your repo has multiple packages at all.
The scripts — "build": "turbo run build" and so on — are wrappers. When you type npm run build at the root, you are really running turbo run build, which tells Turborepo to run the build script in every package in the repo, in the right order, with caching.
And turbo in devDependencies is the Turborepo tool itself — it gets installed when you run npm install or pnpm install at the root.
The pnpm-workspace.yaml file
packages:
- "apps/*"
- "packages/*"
If AI uses pnpm (which it often does with Turborepo because they work well together), this file replaces the workspaces field in package.json. It does the same thing: it tells pnpm which folders contain separate packages. Think of it as a guest list for a building — this file lists which rooms are occupied apartments versus storage closets.
The turbo.json file — the one everyone stares at
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}
This is the file that confuses most people. Let us decode it piece by piece.
"$schema" — This line just tells your code editor how to validate and autocomplete the file. You can ignore it entirely. It does not affect how the build works.
"tasks" — This is where you define your pipeline: the rules Turborepo follows when running tasks. Each key ("build", "dev", "lint", "test") corresponds to an npm script that exists inside your packages.
"dependsOn": ["^build"] — This is the most important and most confusing line. The ^ symbol means "the same task in packages that this package depends on." In plain English: before building the web app, first build the shared package it imports from. You cannot paint a room that does not exist yet. The ^ enforces that order automatically — you never have to think about it.
"outputs": ["dist/**", ".next/**"] — This tells Turborepo what files to save when caching a build. If Turborepo is going to skip this package's build next time (because nothing changed), it needs to know which output files to restore. These are the built files — the compiled JavaScript that gets deployed. Without this, caching would not work correctly.
"cache": false on the dev task — The development server runs continuously (it watches for changes and live-reloads). You cannot cache a process that never ends. This line tells Turborepo not to try.
"persistent": true — This tells Turborepo that the dev task runs forever (it is a long-running process, not a one-time build). Turborepo uses this information to start all persistent tasks in parallel properly.
Individual package package.json files
// apps/web/package.json
{
"name": "@my-app/web",
"version": "0.0.1",
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint"
},
"dependencies": {
"@my-app/shared": "workspace:*",
"next": "^14.0.0",
"react": "^18.0.0"
}
}
A couple of things are unusual here compared to a normal single-package project.
The "name" uses a scoped format: @my-app/web. This is how packages in a monorepo identify each other. The @my-app/ prefix groups all the packages under one "namespace." It is like naming rooms in a house: "House A / Kitchen" instead of just "Kitchen" — useful when you have multiple houses.
The dependency "@my-app/shared": "workspace:*" is how the web app says "I depend on the shared package that lives right here in this same repo." The workspace:* means "use whatever version is in the workspace" — it links to the local package instead of downloading from the internet. This is the magic that lets packages in a monorepo share code with each other.
Understanding turbo run build
When you type turbo run build (or npm run build at the root, which is the same thing), here is exactly what happens, step by step.
Step 1: Turborepo reads your workspace. It looks at all the package.json files across your repo and builds a map of which packages depend on which other packages.
Step 2: Turborepo reads turbo.json. It checks the task rules — specifically, the dependsOn field — to understand what needs to happen before what.
Step 3: Turborepo checks the cache. For each package that needs building, it computes a hash of all the input files (your source code, config files, environment variables). If that hash matches a previous build, Turborepo skips that package and restores the cached output instead of rebuilding.
Step 4: Turborepo runs the remaining tasks in order. Packages with no dependencies run first (your shared library). Once they finish, packages that depend on them start building — potentially in parallel if multiple packages depend on the same thing and can build independently of each other.
In your terminal, this looks like:
$ turbo run build
• Packages in scope: @my-app/api, @my-app/shared, @my-app/web
• Running build in 3 packages
• Remote caching disabled
@my-app/shared:build: cache miss, executing...
@my-app/shared:build: Successfully compiled TypeScript...
@my-app/shared:build: Done in 2.1s
@my-app/api:build: cache miss, executing...
@my-app/web:build: cache miss, executing...
@my-app/api:build: Done in 4.3s
@my-app/web:build: Done in 8.7s
Tasks: 3 successful, 3 total
Cached: 0 cached, 3 total
Time: 11.2s
That was a cold run — nothing was cached yet. Now run turbo run build again without changing any files:
$ turbo run build
• Packages in scope: @my-app/api, @my-app/shared, @my-app/web
@my-app/shared:build: cache hit, replaying logs
@my-app/api:build: cache hit, replaying logs
@my-app/web:build: cache hit, replaying logs
Tasks: 3 successful, 3 total
Cached: 3 cached, 3 total
Time: 412ms
11 seconds became 412 milliseconds. That is caching in action. Nothing changed, so nothing rebuilt. On a real project with 10 packages that takes 4 minutes to build from scratch, a cached run might take under a second.
The painter analogy: Imagine you painted every room in a house. A week later, someone asks you to "paint the house." You walk through each room with a photograph from last week. Kitchen looks identical — skip it. Living room: someone moved a couch and scuffed the wall — repaint it. Turborepo does the same walk-through with your code, comparing now to the last build, and only redoes the rooms (packages) that actually changed.
What AI Gets Wrong About Turborepo
AI is good at scaffolding Turborepo projects, but it makes a set of predictable mistakes that will cost you time if you do not know what to look for.
Forgetting to define outputs for custom tasks
This is the most common mistake. AI generates a turbo.json with outputs defined for build, but then you add a custom script — like "generate": "prisma generate" — and add it to turbo.json without specifying what files it outputs. Turborepo cannot cache what it cannot find.
// ❌ AI sometimes generates this — no outputs defined
"tasks": {
"generate": {
"dependsOn": ["^generate"]
}
}
// ✅ Correct — tell Turborepo what files to cache
"tasks": {
"generate": {
"dependsOn": ["^generate"],
"outputs": ["node_modules/.prisma/**", "src/generated/**"]
}
}
Using npm install in a pnpm monorepo
AI often mixes package manager commands. If your project has a pnpm-workspace.yaml file, you are using pnpm. Running npm install will create a package-lock.json that conflicts with pnpm-lock.yaml, and you will get strange dependency errors. The rule is simple: one package manager per repo. Look at which lock file exists and use the matching command.
# pnpm-lock.yaml exists → use pnpm
pnpm install
pnpm run build
# package-lock.json exists → use npm
npm install
npm run build
Running package scripts from the wrong directory
In a monorepo, there are two places you can run commands: the root (top of the repo) and inside individual packages. AI sometimes tells you to cd apps/web && npm run build — which builds only the web app, in isolation, without building the shared package it depends on first. This fails with "cannot find module @my-app/shared" errors because the shared package was never built.
The correct approach: run commands from the root using Turborepo, which handles the dependency order for you.
# ❌ Running from inside a package — misses dependencies
cd apps/web
npm run build
# Error: Cannot find module '@my-app/shared'
# ✅ Running from root with Turborepo — handles everything
turbo run build
# Builds shared first, then web
Not adding new packages to the workspace definition
When AI adds a new package to your monorepo — say, a new packages/email/ folder — it sometimes forgets to add it to the workspace configuration. The result: the package exists on disk but your package manager does not know it is there. When other packages try to import from it using workspace:*, the install fails.
# Check pnpm-workspace.yaml — does it include your new package?
packages:
- "apps/*"
- "packages/*" ← this glob should catch packages/email automatically
# If AI added it in a weird location like packages/email/src,
# the glob won't match. Make sure the folder structure is correct.
Generating turbo.json with the old pipeline key
Turborepo 2.0 changed "pipeline" to "tasks" in turbo.json. AI trained on older code sometimes generates the old format, which causes an error when you run turbo.
// ❌ Old format (Turborepo 1.x) — AI generates this sometimes
{
"pipeline": {
"build": { "dependsOn": ["^build"] }
}
}
// ✅ New format (Turborepo 2.x)
{
"tasks": {
"build": { "dependsOn": ["^build"] }
}
}
How to Debug Turborepo with AI
When something breaks in a Turborepo project, follow these steps before asking AI for help. Understanding what is actually wrong will make AI's answers dramatically more useful.
Step 1: Read the error carefully — which package is it in?
Turborepo prefixes every log line with the package name: @my-app/web:build: Error: .... The first thing to identify is which package failed. Once you know that, you can navigate to that package's folder and debug it in isolation.
# Run with verbose output to see all logs
turbo run build --verbosity=2
# Run only one specific package's build
turbo run build --filter=@my-app/web
Step 2: Disable caching to rule out stale cache issues
If your build fails but you recently had a successful build, a stale cache entry might be restoring broken output. Force a full rebuild by bypassing the cache:
# Skip the cache for this run (does not delete it)
turbo run build --force
# Delete the local cache entirely and start fresh
rm -rf .turbo
Step 3: Check the dependency graph
If packages are building in the wrong order, or a package is not building at all, ask Turborepo to show you its understanding of your project:
# Show the task graph — which tasks depend on which
turbo run build --dry=json
This outputs a JSON description of what Turborepo would run and in what order, without actually running it. It is the equivalent of asking a contractor to walk you through the plan before breaking ground.
Step 4: Tell AI exactly what you are seeing
When you ask AI for help with a Turborepo error, include the full error output including the package name prefix. "My build is broken" is hard to diagnose. "I get this exact error" is something AI can work with.
Prompt That Actually Gets Help
I have a Turborepo monorepo with pnpm workspaces.
When I run `turbo run build`, I get this error:
@my-app/web:build: Error: Cannot find module '@my-app/shared'
@my-app/web:build: at Function.Module._resolveFilename
Here is my turbo.json:
[paste turbo.json]
Here is apps/web/package.json:
[paste package.json]
The shared package is at packages/shared/ and has its own package.json.
What is wrong?
Step 5: Verify workspace linking worked
The most common root cause of "cannot find module" errors in monorepos is that the packages are not properly linked. Run this to verify:
# With pnpm — list all workspace packages
pnpm list --filter "@my-app/*"
# Check that node_modules symlinks are correct
ls -la apps/web/node_modules/@my-app/
If @my-app/shared is missing from that list, the workspace link did not form. Run pnpm install from the root again to re-establish it.
Turborepo vs. Running Scripts Manually
You might be wondering: could I just write a shell script that builds my packages in the right order? Yes — and many teams did exactly that before Turborepo existed. Here is why Turborepo is better.
| Approach | Manual Shell Script | Turborepo |
|---|---|---|
| Build order | You define it manually, you maintain it manually | Inferred from dependsOn in turbo.json automatically |
| Caching | None — rebuilds everything every time | Built in — skips unchanged packages |
| Parallelism | Hard to implement correctly | Automatic — Turborepo figures out what can run in parallel |
| Remote caching | You build it yourself | Built in — share cache with teammates or CI |
| Adding a new package | Update the script, retest the order | Add it to the workspace — Turborepo figures out the rest |
| Error when order is wrong | Cryptic "module not found" that is hard to trace | Clear task graph lets you see the problem immediately |
The short version: Turborepo does in 50 lines of turbo.json what would take you a weekend to build with shell scripts — and your version would have bugs and need maintenance every time the repo changed. Let Turborepo handle the orchestration.
Remote Caching: The Next Level
Everything described so far is local caching — the cache lives on your own machine in a .turbo folder. Turborepo also supports remote caching, which is a shared cache that lives in the cloud.
Here is why that matters: without remote caching, every time a new developer joins your project and clones the repo, they have to do a full build from scratch — no cached outputs exist on their machine yet. With remote caching, they download the cached build outputs from the shared cache instead. A build that takes 10 minutes for a new developer with no cache becomes a 30-second download.
The same applies to CI (automated testing pipelines). If you push a commit that only changes the frontend, the CI system can pull cached backend build outputs from the remote cache instead of rebuilding the backend from scratch.
Vercel offers free remote caching for Turborepo projects. You connect it with:
# Link your project to Vercel's remote cache
npx turbo login
npx turbo link
After that, every turbo run build automatically checks the remote cache before building. For teams and CI pipelines, this is a significant speed improvement. For solo projects on your own machine, local caching is usually enough.
When Should You Actually Use Turborepo?
Turborepo adds a layer of configuration. For some projects, it is absolutely worth it. For others, it is overkill. Here is how to decide.
Use Turborepo when:
- You have two or more apps or packages in one repository that share code
- AI scaffolded your project as a monorepo (you see
apps/andpackages/folders) - Build times are getting long and you want caching to speed them up
- You are deploying multiple services that all live in the same repo
- You are collaborating with a team and want shared build cache
Do not use Turborepo when:
- You have one app in one folder — no shared packages, no multiple services
- You are following a tutorial that uses a simple single-package structure
- Your project is small enough that build speed is not a concern
- You are still learning the basics and the extra configuration layer is confusing things
The rule of thumb: If your project folder has both an apps/ and a packages/ directory, Turborepo is probably appropriate. If it has a single src/ folder, you almost certainly do not need it. When AI generates a monorepo structure and you did not ask for one, it is worth asking AI to simplify it to a single-app structure before you start — unless you genuinely need multiple separate packages.
Frequently Asked Questions
Do I need Turborepo for every project?
No. Turborepo only makes sense for monorepos — projects where multiple apps or packages live in a single repository. If you have one app in one folder, Turborepo adds complexity with no benefit. It becomes valuable when you have two or more packages that share code, like a frontend app, a backend API, and a shared component library all in one repo.
What is turbo.json and do I need to edit it?
turbo.json is the configuration file that tells Turborepo how your tasks relate to each other and what outputs to cache. In most AI-generated projects you do not need to edit it — the defaults work fine. You only need to modify turbo.json when you add new custom scripts to your packages that Turborepo does not know about, or when you want to tune caching behavior.
Why does turbo run build say "cache hit" and finish in 0 seconds?
That is Turborepo's caching working correctly. If none of your source files changed since the last build, Turborepo skips the build entirely and uses the saved output from last time. It is like a contractor who keeps photos of completed work — if the room looks identical to the photo, no need to repaint. A 0-second build with "cache hit" is a success, not an error.
What is the difference between turbo run build and npm run build?
npm run build runs the build script in a single package.json. turbo run build runs the build script across every package in your monorepo, in the right order, skipping anything that has not changed. In a monorepo with five packages, npm run build from the root often does nothing or errors. turbo run build orchestrates all five packages automatically.
Can I use Turborepo without pnpm or with regular npm?
Yes. Turborepo works with npm, yarn, and pnpm workspaces. However, AI often pairs Turborepo with pnpm because pnpm workspaces are faster and use less disk space in monorepos. If your project has a pnpm-workspace.yaml file, you are using pnpm. If it has workspaces defined in package.json, you are using npm or yarn workspaces. Turborepo sits on top of whichever workspace tool you pick.
What to Learn Next
Now that you understand what Turborepo does and why AI reaches for it, here is where to go next to fill in the surrounding context:
- What Is a Monorepo? — Turborepo manages monorepos, but you should understand what a monorepo is and when it makes sense for your project before committing to one.
- What Is pnpm? — AI almost always pairs Turborepo with pnpm. Understanding pnpm's workspace features will help you make sense of why packages can reference each other with
workspace:*. - What Is Vercel? — Turborepo is made by Vercel, and Vercel offers free remote caching for Turborepo projects. If you are deploying a Turborepo monorepo, Vercel is the natural deployment target.
- What Is npm? — npm's workspace feature is the foundation that Turborepo builds on. Understanding how npm workspaces link local packages together helps the
workspace:*dependency notation make sense.