← All articles
BUILD TOOLS Turborepo: Fast Monorepo Builds for TypeScript Projects 2026-02-28 · 4 min read · turborepo · monorepo · typescript

Turborepo: Fast Monorepo Builds for TypeScript Projects

Build Tools 2026-02-28 · 4 min read turborepo monorepo typescript build-tools caching ci-cd

Monorepos are excellent for sharing code across multiple packages and apps, but they have an inherent problem: as they grow, builds get slow. Run tests on a change to a utility package, and you might trigger rebuilds of 15 apps that depend on it — most of which haven't changed.

Turborepo solves this with content-aware caching and parallel task execution. It only runs tasks that need to run, caches outputs locally and remotely, and can parallelize independent tasks across your available CPU cores.

Turborepo build output showing task graph, cache hits, and parallel execution timing

What Turborepo Does

Given a monorepo with packages ui, utils, api, and web:

The result: builds that take 10 minutes can drop to 30 seconds — even on CI where nothing is cached locally.

Monorepo Structure

Turborepo works with npm, Yarn, pnpm, and Bun workspaces. Example structure:

my-monorepo/
├── apps/
│   ├── web/           # Next.js frontend
│   └── api/           # Express/Hono API
├── packages/
│   ├── ui/            # Shared React components
│   ├── utils/         # Shared utilities
│   └── config/        # Shared configs (ESLint, TypeScript, etc.)
├── package.json       # Root workspace config
└── turbo.json         # Turborepo configuration

Installation

In a new project

npx create-turbo@latest
# Or with Bun
bunx create-turbo@latest

In an existing monorepo

# npm/Yarn/pnpm
npm install turbo --save-dev -W

# Bun
bun add -d turbo

Configuration: turbo.json

The core Turborepo config defines tasks, their dependencies, and caching behavior.

Basic configuration

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "test": {
      "outputs": ["coverage/**"]
    },
    "lint": {},
    "dev": {
      "persistent": true,
      "cache": false
    }
  }
}

Key concepts

dependsOn: Specifies what must run before this task.

outputs: Files to cache when this task completes. On cache hit, these files are restored without re-running the task.

cache: Set to false for tasks that should always run (like dev servers).

persistent: Long-running tasks that shouldn't be treated as completed (watchers, dev servers).

Advanced configuration

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**", "!dist/**/*.map"],
      "env": ["NODE_ENV", "API_URL"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "package.json"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**/*.ts", "**/__tests__/**", "jest.config.*"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "lint": {
      "dependsOn": []
    },
    "db:generate": {
      "cache": false
    }
  }
}

env: Environment variables that affect the cache key — if API_URL changes, the build runs again.

inputs: Limit which files affect the cache key. By default, all files in the package directory are considered.

Running Tasks

# Run build in all packages
turbo run build
# Or with Bun
bunx turbo build

# Run multiple tasks
turbo run lint test build

# Run in specific packages only
turbo run build --filter=web
turbo run test --filter=./apps/*  # All apps
turbo run build --filter=...web   # web and everything it depends on

# Force re-run ignoring cache
turbo run build --force

# Dry run (show what would run)
turbo run build --dry-run

Filter syntax

Filter Matches
--filter=web Package named "web"
--filter=./apps/* All packages in apps/
--filter=...web web + all its dependents
--filter=web... web + all its dependencies
--filter=[HEAD^1] Packages changed since last commit
--filter=[main...HEAD] Packages changed since main branch

The [HEAD^1] filter is powerful for CI: only run tests for packages that actually changed in the PR.

Package Dependencies

For Turborepo to understand the task graph, packages must declare their workspace dependencies:

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "@myrepo/ui": "*",
    "@myrepo/utils": "*"
  }
}
// packages/ui/package.json
{
  "name": "@myrepo/ui",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs"
  }
}

Turborepo reads these dependencies to build the task graph and determine execution order.

Remote Caching

Local caching helps on developer machines. Remote caching shares the cache across all machines — CI, other developers, staging environments.

Vercel Remote Cache (official, free)

# Link to Vercel (one-time setup)
turbo login
turbo link

# CI: set TURBO_TOKEN and TURBO_TEAM environment variables

In GitHub Actions:

- name: Build
  run: turbo run build
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Self-hosted remote cache (Ducktape/Turborepo Remote Cache)

For fully self-hosted remote caching, use the open-source ducktape or turborepo-remote-cache project:

# Deploy ducktape
docker run -p 3000:3000 \
  -e STORAGE_PROVIDER=local \
  -e STORAGE_PATH=/data \
  fox1t/turborepo-remote-cache

Configure Turborepo to use it:

// turbo.json
{
  "remoteCache": {
    "apiUrl": "https://turbo-cache.yourdomain.com"
  }
}

CI/CD Integration

GitHub Actions

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # For HEAD^1 filtering

      - uses: oven-sh/setup-bun@v2

      - name: Install dependencies
        run: bun install --frozen-lockfile

      - name: Build changed packages
        run: bunx turbo run build --filter=[HEAD^1]
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Test changed packages
        run: bunx turbo run test --filter=[HEAD^1]
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Parallel jobs for large monorepos

For very large repos, split tasks across parallel CI runners:

strategy:
  matrix:
    task: [lint, typecheck, test]
steps:
  - run: bunx turbo run ${{ matrix.task }}

Turborepo vs. Nx

Turborepo Nx
Configuration Simple JSON More complex
Generator/scaffolding None Built-in
Caching Excellent Excellent
Remote cache Vercel (free) or self-hosted Nx Cloud (paid) or self-hosted
Affected detection Git-based Git-based + static analysis
Language support Any Any
Learning curve Low Higher
Plugin ecosystem Minimal Extensive

Turborepo is simpler to set up and configure. Nx has more features for large organizations (code generators, project graph visualization, enforced architecture boundaries). For most TypeScript projects, Turborepo is the right choice.

Migrating an Existing Monorepo

  1. Install Turborepo: npm install turbo -D -W
  2. Create turbo.json with your task definitions
  3. Identify shared outputs (dist/, .next/, build/)
  4. Run turbo run build and verify behavior
  5. Enable remote caching
  6. Update CI to use --filter=[HEAD^1]

The migration is incremental — Turborepo wraps your existing npm scripts, so you don't need to change the scripts themselves.


More TypeScript tooling guides at DevTools Guide newsletter.