Turborepo can make your monorepo feel faster by running tasks in parallel and only rebuilding what’s changed.
Let’s see it in action. Imagine you have a monorepo with two Next.js apps, web and admin, and a shared ui library.
my-monorepo/
├── apps/
│ ├── web/
│ │ ├── pages/
│ │ └── next.config.js
│ └── admin/
│ ├── pages/
│ └── next.config.js
├── packages/
│ └── ui/
│ ├── components/
│ └── package.json
└── turbo.json
First, you’ll need to install Turborepo as a dev dependency in your root package.json:
npm install --save-dev turbo
# or
yarn add --dev turbo
# or
pnpm add -D turbo
Then, create a turbo.json file at the root of your monorepo. This file is the heart of your Turborepo configuration.
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false
},
"lint": {}
}
}
This turbo.json defines a pipeline with three tasks: build, dev, and lint.
-
The
buildtask has a few key settings:dependsOn: ["^build"]: This is crucial. It tells Turborepo that to build any app or package, it must first build any of its dependencies that also have abuildtask. The^symbol means "dependencies of dependencies" or "transitive dependencies". So, ifwebdepends onui, anduihas abuildscript,web’sbuildwill wait forui’sbuildto complete.outputs: ["dist/**", ".next/**"]: This tells Turborepo which directories contain the outputs of thebuildtask. Turborepo will cache these outputs. If you runbuildagain and nothing has changed in the inputs, Turborepo will use the cached output, making subsequent builds lightning fast..next/**is specific to Next.js builds.
-
The
devtask hascache: false. This means Turborepo won’t cache the results of thedevcommand. Development servers are typically long-running and change constantly, so caching them doesn’t make much sense. Runningturbo run devwill start all your dev servers in parallel. -
The
linttask has no special configuration, meaning it will run and its outputs won’t be cached by default.
Now, you need to ensure your individual app and package package.json files are set up to use these Turborepo tasks. In each package.json (for apps/web, apps/admin, and packages/ui), add scripts that mirror the Turborepo pipeline:
In apps/web/package.json:
{
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "next lint"
},
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0",
"next": "13.5.6",
"@repo/ui": "workspace:*" // Example dependency on shared UI
}
// ...
}
In apps/admin/package.json:
{
"name": "admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "next lint"
},
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0",
"next": "13.5.6",
"@repo/ui": "workspace:*" // Example dependency on shared UI
}
// ...
}
In packages/ui/package.json:
{
"name": "@repo/ui",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "echo 'Building UI...'", // Replace with actual build command if needed
"lint": "eslint --ext .js,.jsx,.ts,.tsx ."
},
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
// ...
}
Notice the "@repo/ui": "workspace:*" dependency in the apps. This tells npm/yarn/pnpm that ui is part of the monorepo. Turborepo understands this workspace relationship.
With this setup, you can now run commands from your monorepo root:
turbo run build: This will buildui, thenweb, andadminin parallel, respecting their dependencies. Ifuihasn’t changed since the lastbuild, Turborepo will skip its build and use the cached output.turbo run dev: This will startnext devfor bothwebandadminsimultaneously.turbo run lint: This will run the linting tasks for all packages and apps.
The real magic happens when you run turbo run build again after making a change. Turborepo analyzes the Git history and your outputs configuration. It only rebuilds the packages that have changed and their dependents. If you only modified packages/ui, only ui would be rebuilt, and then web and admin would be rebuilt using the new ui components. If you then made a change in apps/web that didn’t depend on ui, ui wouldn’t be rebuilt, and web would be rebuilt, but admin might not be rebuilt at all if its inputs haven’t changed and it doesn’t depend on web.
The turbo.json pipeline is not just about defining tasks; it’s also about managing execution flow and caching. The ^build dependency ensures that when you run turbo build, Turborepo correctly identifies the order of operations: build shared dependencies first, then build the apps that rely on them. This parallel execution, combined with intelligent caching based on file changes and defined outputs, is what dramatically speeds up your monorepo’s build and development workflows. It effectively turns your monorepo into a single, highly optimized build system, rather than a collection of independent projects.
Turborepo’s caching mechanism is designed to be robust. It hashes the content of your input files and the command itself to generate a cache key. If either the inputs or the command change, a new cache key is generated, and the task is re-executed. This is why specifying accurate outputs in turbo.json is so important; it tells Turborepo what to cache after a successful run.
When you run turbo run build, Turborepo first looks at the build script in each package.json. It then checks if the outputs for that specific task (e.g., .next directory for Next.js) already exist and are valid based on its cache. If a cache hit occurs, it uses the cached artifacts. If not, it executes the build script. Crucially, before executing a task, it ensures all its dependsOn tasks have completed successfully. This granular control over task dependencies and caching is what makes Turborepo so powerful for complex monorepos.
The next challenge you’ll likely face is managing different environments or configurations across your apps.