Skip to main content

Command Palette

Search for a command to run...

Turborepo: Fast Monorepo Builds for TypeScript Projects

Published
6 min read
D
Practical guides for developers: TypeScript, developer tools, CI/CD, and modern web development. We cover the tools that make devs more productive.

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:

  • If you change utils, Turborepo rebuilds utils and anything that depends on it
  • If nothing changed since the last build, Turborepo replays cached outputs instantly
  • Independent tasks (like testing api and testing web) run in parallel
  • With remote caching, CI gets the same cache benefits as local development

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.

  • "^build" means "run build in all dependencies first" (topological ordering)
  • "build" (without ^) means "run build in the same package first"
  • [] means the task has no dependencies

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

FilterMatches
--filter=webPackage named "web"
--filter=./apps/*All packages in apps/
--filter=...webweb + 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

TurborepoNx
ConfigurationSimple JSONMore complex
Generator/scaffoldingNoneBuilt-in
CachingExcellentExcellent
Remote cacheVercel (free) or self-hostedNx Cloud (paid) or self-hosted
Affected detectionGit-basedGit-based + static analysis
Language supportAnyAny
Learning curveLowHigher
Plugin ecosystemMinimalExtensive

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.

More from this blog

DevTools Guide

183 posts