Introduction to Turborepo
Turborepo is a high-performance build system for JavaScript and TypeScript codebases. Originally developed by Vercel and now maintained as an open-source project, it is designed to manage monorepos efficiently by orchestrating tasks across multiple packages and applications. Turborepo leverages caching, parallelization, and dependency-aware scheduling to dramatically reduce build and test times.
At its core, Turborepo does not replace your package manager. Instead, it works alongside tools like npm, pnpm, yarn, or bun to provide a unified task runner that understands the relationships between the packages in your workspace. This means you can run a single command like turbo run build and Turborepo will figure out the correct order to build every package, skip packages that have not changed, and cache the results for future runs.
Why Turborepo Matters
As monorepos grow, the complexity of managing interdependent packages increases exponentially. Without a proper build system, you face several challenges: redundant builds, slow CI pipelines, inconsistent task definitions, and difficulty understanding which packages depend on which. Turborepo addresses all of these problems with a declarative configuration model and an intelligent execution engine.
The key benefits include significantly faster local development through content-addressable caching, simplified CI/CD pipelines through a single entry point, predictable task execution through dependency graphs, and remote caching that allows team members and CI runners to share build artifacts. For teams working with large codebases, these improvements can translate to hours of saved development time each week.
Getting Started with Turborepo
To begin using Turborepo, you need an existing monorepo or you can create a new one. Turborepo works with any workspace-compatible package manager. The fastest way to start is by using the create-turbo CLI tool, which scaffolds a complete monorepo with sensible defaults.
npx create-turbo@latest
This command will prompt you to select a package manager, a project name, and whether you want TypeScript or JavaScript. The generated structure typically looks like this:
my-turborepo/
├── apps/
│ ├── web/
│ ├── docs/
│ └── api/
├── packages/
│ ├── ui/
│ ├── eslint-config/
│ └── typescript-config/
├── turbo.json
├── package.json
└── pnpm-workspace.yaml
If you are adding Turborepo to an existing monorepo, you can install it as a development dependency in the root of your workspace:
npm install turbo --save-dev
Then create a turbo.json file at the root of your project. This file is the heart of your Turborepo configuration and defines all the tasks, dependencies, and caching behavior for your monorepo.
The turbo.json Configuration File
The turbo.json file is where you define how Turborepo should run tasks across your workspace. It uses a JSON-based schema that is both human-readable and machine-validatable. Let us examine a complete configuration file and break down each section.
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["DATABASE_URL", "STRIPE_SECRET_KEY"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}
Understanding the Schema Reference
The $schema field is optional but highly recommended. It enables autocomplete and validation in editors like VS Code. By pointing to the official schema URL, your editor will warn you about invalid properties, typos, and missing required fields as you type.
Global Dependencies and Environment Variables
The globalDependencies array tells Turborepo that certain files, when changed, should invalidate the cache for all tasks. This is useful for environment files, root-level configuration, or any file that affects every package. The globalEnv array works similarly but for environment variables. If any of these environment variables change between runs, Turborepo will treat the task as changed and re-run it.
{
"globalDependencies": [
"**/.env.*local",
"tsconfig.base.json",
"turbo.json"
],
"globalEnv": [
"NODE_ENV",
"CI",
"VERCEL_ENV"
]
}
Defining Tasks
The tasks object is the most important part of your configuration. Each key represents a task name that corresponds to a script in your package.json files. When you run turbo run build, Turborepo looks for a build script in every package's package.json and executes them according to the rules you define here.
The dependsOn Property
The dependsOn property defines task dependencies. There are two types of dependencies you can express. The first uses the caret prefix ^ to indicate that a task depends on the same task in upstream packages. For example, "dependsOn": ["^build"] means that before building a package, all of its dependencies must be built first. The second type, without the caret, means the task depends on another task within the same package.
{
"tasks": {
"build": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build", "^build"]
},
"integration-test": {
"dependsOn": ["test"]
}
}
}
In this example, the test task depends on the build task in the same package and the build task in all upstream packages. The integration-test task depends on the test task in the same package, creating a chain of execution.
The outputs Property
The outputs property tells Turborepo which files and directories should be cached after a task completes. This is critical for effective caching. If you do not specify outputs, Turborepo will not cache any build artifacts, and downstream packages will not benefit from cached builds. You can use glob patterns and negate patterns with the ! prefix.
{
"tasks": {
"build": {
"outputs": [
"dist/**",
"build/**",
".next/**",
"!.next/cache/**",
"coverage/**"
]
}
}
}
The inputs Property
The inputs property is the complement to outputs. It defines which files should be considered when determining whether a task's cache is still valid. By default, Turborepo considers all files in a package, but specifying inputs can make cache hits more precise and improve performance by reducing the amount of hashing work Turborepo needs to do.
{
"tasks": {
"test": {
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"test/**/*.ts",
"test/**/*.tsx",
"package.json",
"tsconfig.json"
]
}
}
}
The env Property
Environment variables can affect build output, so Turborepo needs to know about them to cache correctly. The env property lists environment variables that, when changed, should invalidate the cache for that specific task. Unlike globalEnv, which affects all tasks, env is scoped to a single task definition.
{
"tasks": {
"build": {
"env": [
"DATABASE_URL",
"STRIPE_SECRET_KEY",
"NEXT_PUBLIC_API_URL"
]
}
}
}
Cache and Persistent Tasks
Some tasks should never be cached. Development servers, watch mode scripts, and interactive tasks fall into this category. You can disable caching for a task by setting "cache": false. Additionally, tasks that run indefinitely, like dev servers, should be marked as "persistent": true. This tells Turborepo not to wait for these tasks to complete before considering the run finished.
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"watch": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}
Package-Level Configuration
While turbo.json defines global task rules, individual packages can override or extend these rules using the turbo key in their own package.json files. This is useful when a specific package has unique build outputs, environment variables, or dependencies.
{
"name": "@my-org/web-app",
"version": "1.0.0",
"scripts": {
"build": "next build",
"dev": "next dev"
},
"turbo": {
"outputs": [".next/**", "!.next/cache/**"],
"dependsOn": ["^build", "@my-org/ui#build"],
"env": ["NEXT_PUBLIC_ANALYTICS_ID"]
}
}
In this example, the web-app package overrides the default outputs for the build task and adds a specific dependency on the build task of the @my-org/ui package using the package#task syntax. This syntax allows you to express dependencies on specific packages rather than all upstream packages.
Running Tasks with Turbo
Once your configuration is in place, you run tasks using the turbo run command. Turborepo will analyze your workspace, build a dependency graph, and execute tasks in the optimal order with maximum parallelism.
# Run the build task across all packages
turbo run build
# Run multiple tasks in sequence
turbo run lint test build
# Run a task for a specific package and its dependencies
turbo run build --filter=@my-org/web-app
# Run a task and force rebuild (ignore cache)
turbo run build --force
# Run tasks in parallel with a concurrency limit
turbo run build --concurrency=4
# Continue running other tasks even if one fails
turbo run build --continue
Filtering Packages
The --filter flag is one of Turborepo's most powerful features. It allows you to target specific packages, their dependencies, or their dependents. This is invaluable for running tasks only on the parts of your monorepo that are relevant to your current work.
# Build only the web app and its dependencies
turbo run build --filter=@my-org/web-app...
# Build only packages that depend on the UI package
turbo run build --filter=...@my-org/ui
# Build packages that changed since the main branch
turbo run build --filter=...[origin/main]
# Build packages changed in a specific commit range
turbo run build --filter=...[HEAD^1]
# Build a specific directory
turbo run build --filter=./apps/web
# Combine filters
turbo run build --filter=@my-org/web-app... --filter=!@my-org/legacy-app
Remote Caching
Local caching speeds up development on a single machine, but remote caching takes Turborepo to the next level by sharing cache artifacts across your entire team and CI infrastructure. When one developer builds a package, the cached output is uploaded to a remote server. When another developer or CI runner needs to build the same package with the same inputs, Turborepo downloads the cached output instead of rebuilding.
Vercel provides a free remote cache for Turborepo users. To enable it, simply log in with your Vercel account:
npx turbo login
npx turbo link
For self-hosted or enterprise setups, you can configure a custom remote cache server using environment variables. Turborepo supports any cache server that implements the Vercel remote cache API specification.
# .env file at the root of your project
TURBO_API=https://your-cache-server.com
TURBO_TOKEN=your-secret-token
TURBO_TEAM=your-team-name
You can also configure remote caching in your turbo.json or through a .turbo/config.json file for more complex setups:
{
"apiURL": "https://your-cache-server.com",
"teamSlug": "your-team-name"
}
Environment Variable Management
Turborepo provides a helpful command to audit which environment variables are used across your monorepo. This is especially useful when setting up CI pipelines or debugging cache misses caused by untracked environment variables.
# List all environment variables used by a task
turbo run build --dry=json | jq '.tasks[].resolvedTaskDefinition.env'
# Use the env command to see what env vars turbo knows about
turbo info
For frameworks like Next.js that have specific environment variable conventions, Turborepo automatically detects and tracks variables prefixed with NEXT_PUBLIC_. However, it is still best practice to explicitly declare all environment variables in your configuration to avoid surprises.
Workspace Configuration
Your package manager's workspace configuration works hand in hand with Turborepo. Each package manager has its own way of defining workspaces, and Turborepo reads these configurations to understand your monorepo structure.
For pnpm, you use a pnpm-workspace.yaml file:
packages:
- "apps/*"
- "packages/*"
- "tools/*"
For npm and yarn, workspaces are defined in the root package.json:
{
"name": "my-turborepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*",
"tools/*"
],
"devDependencies": {
"turbo": "^2.0.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean"
}
}
Best Practices
- Be explicit about outputs: Always specify the
outputsarray for build tasks. Missing outputs means no caching, which defeats one of the primary benefits of Turborepo. Review your build tool's documentation to understand exactly which directories contain build artifacts. - Use inputs for precision: Defining
inputsfor tasks like tests can significantly improve cache hit rates. If your tests only depend on source files and test files, there is no reason to invalidate the cache when a README changes. - Declare all environment variables: Any environment variable that affects build output must be declared in either
globalEnvor task-levelenv. Undeclared variables lead to false cache hits and subtle bugs. - Mark persistent tasks correctly: Always set
"persistent": truefor dev servers and watch tasks. Failing to do so can cause Turborepo to hang waiting for a process that will never exit. - Use filtering in CI: Leverage
--filterwith branch comparison to only build and test packages that have changed. This can dramatically reduce CI times for large monorepos. - Enable remote caching early: Set up remote caching as soon as possible. The compounding benefits across a team and CI pipeline are substantial, and it requires minimal configuration.
- Keep tasks granular: Instead of creating a single monolithic build script, break tasks into smaller, cacheable units. For example, separate code generation, compilation, and bundling into distinct tasks.
- Version your configuration: Pin your Turborepo version in
package.jsonand update deliberately. Major versions can introduce breaking changes to the configuration schema. - Use dry runs for debugging: The
--dryand--dry=jsonflags let you inspect what Turborepo would do without actually running anything. This is invaluable for debugging dependency graphs and cache behavior. - Regularly audit cache behavior: Use
--graphto generate a visual representation of your task graph. This helps identify unexpected dependencies and optimization opportunities.
Advanced Configuration
Task Pass-Through Arguments
Turborepo supports passing arguments through to the underlying task scripts using the -- separator. This is useful for passing flags to your test runner or build tool without modifying your package.json scripts.
# Pass --watch flag to the test runner
turbo run test -- --watch
# Pass a specific config file to the build tool
turbo run build -- --config=production.json
Generating Task Graphs
For complex monorepos, visualizing the task dependency graph can help you understand and optimize your build pipeline. Turborepo can generate graph files in multiple formats.
# Generate an HTML visualization
turbo run build --graph=graph.html
# Generate a DOT file for Graphviz
turbo run build --graph=graph.dot
# Generate a JSON representation
turbo run build --graph=graph.json
Conditional Task Execution
You can use package.json scripts to create conditional behavior. For example, only running certain tasks in CI environments:
{
"scripts": {
"build": "turbo run build",
"build:ci": "turbo run build --filter=...[origin/main] --concurrency=8"
}
}
Conclusion
Turborepo is a powerful tool that brings order and speed to monorepo development. By understanding the full range of configuration options available in turbo.json, from task dependencies and caching to environment variables and filtering, you can create a build pipeline that is both fast and maintainable. The key to success with Turborepo is thoughtful configuration: be explicit about your inputs and outputs, declare your environment variables, leverage remote caching, and use filtering to keep your CI pipelines lean. As your monorepo grows, the investment in proper Turborepo configuration pays dividends in developer productivity, CI performance, and overall codebase health. Start with the basics, measure your cache hit rates, and iteratively refine your configuration to squeeze out every possible performance gain.