← Back to DevBytes

Turborepo: Complete Configuration Guide

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles