Managing a Large Codebase at Speed: Turborepo in Production
When a monorepo grows to dozens of packages and CI takes 20 minutes, something has to change. Turborepo's task graph and remote caching cut our pipeline times dramatically — here's the exact configuration we run and the pitfalls we worked around.
As JavaScript projects grow, managing multiple applications and shared packages becomes increasingly complex. Build times slow down, code duplication creeps in, and developer productivity suffers. Turborepo solves these problems by providing a high-performance build system designed specifically for monorepos. In this guide, I'll walk you through setting up a production-ready Turborepo workspace from scratch.
What is Turborepo?
Turborepo is a build system that optimizes JavaScript and TypeScript monorepos through intelligent caching and task parallelization. Instead of rebuilding everything on every change, Turborepo tracks what has changed and only rebuilds what's necessary.
The key benefits include:
- Never do the same work twice: Turborepo caches task outputs and restores them when inputs haven't changed.
- Maximum parallelization: Tasks run simultaneously across all available CPU cores.
- Remote caching: Share cached artifacts across your team and CI pipelines.
- Incremental adoption: Works with your existing package.json scripts and supports npm, yarn, pnpm, and bun.
Installation
Install Turborepo globally to run commands from anywhere in your repository:
npm install turbo --global
Or add it as a dev dependency in your root package.json:
npm install turbo --save-dev
Repository Structure
A Turborepo workspace follows a standard monorepo layout with two main directories:
my-turborepo/ ├── apps/ │ ├── web/ # Next.js application │ │ ├── package.json │ │ └── src/ │ └── api/ # Backend service │ ├── package.json │ └── src/ ├── packages/ │ ├── ui/ # Shared component library │ │ ├── package.json │ │ └── src/ │ ├── utils/ # Shared utilities │ │ ├── package.json │ │ └── src/ │ └── typescript-config/ # Shared TypeScript config │ └── package.json ├── package.json # Root workspace config ├── turbo.json # Turborepo configuration └── pnpm-workspace.yaml # Workspace definition (pnpm)
- apps/ contains deployable applications and services.
- packages/ contains shared libraries, utilities, and configurations.
Workspace Configuration
First, configure your package manager to recognize the workspace structure.
For pnpm (pnpm-workspace.yaml)
packages:
- "apps/*"
- "packages/*"
For npm/yarn (package.json)
{
"name": "my-turborepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"devDependencies": {
"turbo": "^2.0.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
}
}
Turborepo Configuration
The turbo.json file defines how tasks run across your workspace. This is where you configure task dependencies, caching behavior, and outputs.
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
Configuration Options Explained
- globalDependencies: Files that affect all tasks. Changes to these files invalidate the entire cache.
- globalEnv: Environment variables that affect all task hashes.
- dependsOn: Tasks that must complete before this task runs. The
^prefix means "run this task in dependencies first". - outputs: Files and directories to cache after successful task completion.
- cache: Set to false for tasks that should never be cached (like dev servers).
- persistent: Marks long-running tasks that don't exit (like watch mode or dev servers).
Understanding Task Dependencies
The dependsOn configuration controls task execution order. There are three patterns:
Dependency Tasks First (^)
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
The caret (^) means: before building a package, first build all packages it depends on. If web depends on ui, Turborepo builds ui before web.
Same Package Dependencies
{
"tasks": {
"test": {
"dependsOn": ["build"]
}
}
}
Without the caret, the dependency is within the same package. Tests run only after that package's build completes.
Specific Package Tasks
{
"tasks": {
"deploy": {
"dependsOn": ["web#build", "api#build"]
}
}
}
You can reference specific package tasks using the package#task syntax.
Creating Internal Packages
Internal packages let you share code across your monorepo. Here's how to create a shared UI component library.
Package Structure
packages/ui/
├── package.json
├── tsconfig.json
└── src/
├── button.tsx
├── card.tsx
└── index.ts
Package Configuration (packages/ui/package.json)
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint src/"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"typescript": "^5.0.0"
}
}
Using the Package
Add the internal package as a dependency in your application:
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*"
}
}
Then import components directly:
import { Button } from "@repo/ui/button";
import { Card } from "@repo/ui/card";
Caching
Caching is Turborepo's superpower. When you run a task, Turborepo creates a hash from the task's inputs. If the hash matches a previous run, it restores the cached outputs instead of re-executing the task.
What Gets Cached
- Task outputs: Files specified in the
outputsconfiguration. - Terminal logs: Console output from task execution.
What Affects the Cache Hash
- Source files tracked by Git
- Dependencies in package.json and lockfile
- Environment variables specified in
envorglobalEnv - Task configuration in turbo.json
Remote Caching
Share your cache across machines and CI pipelines by connecting to Vercel Remote Cache:
npx turbo login
npx turbo link
Once linked, cached artifacts are uploaded and shared automatically. Your CI pipeline can restore builds computed locally, and teammates benefit from each other's cached work.
Cache Control
turbo run build --force
Use --force to bypass reading from cache while still writing new cache entries.
Running Tasks
Turborepo provides powerful options for running tasks across your workspace.
Run All Packages
turbo run build
Run Multiple Tasks
turbo run build test lint
Filter by Package Name
turbo run build --filter=web
turbo run build --filter=@repo/ui
Filter by Directory
turbo run build --filter="./apps/*"
turbo run lint --filter="./packages/ui"
Include Dependencies
turbo run build --filter=web...
The ... suffix includes all packages that web depends on.
Include Dependents
turbo run build --filter=...ui
The ... prefix includes all packages that depend on ui.
Filter by Git Changes
turbo run build --filter=[HEAD^1]
turbo run build --filter=[main...feature-branch]
CI/CD Integration
Turborepo integrates seamlessly with CI pipelines. Here's a GitHub Actions example:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install
- run: pnpm turbo run build test lint
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
With remote caching enabled, your CI pipeline benefits from locally computed caches, dramatically reducing build times.
Production Checklist
- Enable remote caching: Connect to Vercel Remote Cache or self-host for shared caching across your team.
- Configure outputs correctly: Missing outputs means files won't be cached, causing errors on cache hits.
- Use environment variables wisely: Only include variables that actually affect build output in
envconfiguration. - Set up CI caching: Pass
TURBO_TOKENandTURBO_TEAMto your CI environment. - Filter in CI: Use
--filter=[HEAD^1]to only build what changed in the last commit. - Disable cache for dev tasks: Always set
"cache": falsefor development servers and watch tasks.
Conclusion
Turborepo transforms monorepo management from a DevOps burden into a productivity multiplier. The key takeaways:
- Structure matters: Separate apps and packages for clear boundaries.
- Configure dependencies: Use
^prefix to build dependencies before dependents. - Cache everything: Define outputs for all tasks that produce files.
- Enable remote caching: Share build artifacts across your team and CI.
- Filter strategically: Build only what you need during development and CI.
With proper configuration, Turborepo can reduce build times by 80% or more, letting your team focus on shipping features instead of waiting for builds.
Written by

Technical Lead and Full Stack Engineer leading a 5-engineer team at Fygurs (Paris, Remote) on Azure cloud-native SaaS. Graduate of 1337 Coding School (42 Network / UM6P). Writes about architecture, cloud infrastructure, and engineering leadership.