Why a Monorepo Turborepo Setup Is a Game-Changer in 2026
Managing multiple packages and applications in separate repositories quickly becomes a nightmare. Dependency mismatches, duplicated configurations, and fragmented CI/CD pipelines slow teams down. That is exactly why the monorepo Turborepo setup has become the go-to architecture for JavaScript and TypeScript teams building at scale.
Turborepo is a high-performance build system designed specifically for JavaScript and TypeScript codebases. It simplifies monorepo workflows with intelligent caching, parallel task execution, and minimal configuration. In this guide, we will walk you through every step of setting up a monorepo with Turborepo, from initial scaffolding to a production-ready CI/CD pipeline.
Whether you are starting a brand-new project or migrating existing repositories, this tutorial gives you everything you need to get it right the first time.
What You Will Learn
- How to scaffold a new Turborepo monorepo from scratch
- Workspace configuration with pnpm, npm, or yarn
- Structuring applications and shared packages
- Dependency management across packages
- How Turborepo caching works and how to optimize it
- Setting up CI/CD pipelines with remote caching
- Best practices for scaling your monorepo
Prerequisites
Before diving into the monorepo Turborepo setup, make sure you have the following installed:
- Node.js (v18 or later recommended)
- A package manager: pnpm (recommended), npm, or yarn
- Git for version control
- A code editor like VS Code
We recommend pnpm because it handles workspace dependencies efficiently and uses a content-addressable storage model that saves disk space. Turborepo itself does not handle installing packages, so your choice of package manager matters.
Step 1: Scaffold Your Turborepo Monorepo
The fastest way to start is by using the official create-turbo command:
npx create-turbo@latest my-monorepo
You will be prompted to select a package manager. Choose pnpm for this tutorial. The CLI generates a starter template with:
- A root
turbo.jsonconfiguration file - An
apps/directory for your applications - A
packages/directory for shared libraries - A root
package.jsonwith workspace configuration
After scaffolding, navigate into your project:
cd my-monorepo
pnpm install
Understanding the Default Folder Structure
| Directory/File | Purpose |
|---|---|
apps/web |
A Next.js application (default template) |
apps/docs |
A documentation site or second app |
packages/ui |
Shared UI component library |
packages/eslint-config |
Shared ESLint configuration |
packages/typescript-config |
Shared TypeScript configuration |
turbo.json |
Turborepo pipeline configuration |
pnpm-workspace.yaml |
Defines workspace packages for pnpm |
Step 2: Configure Workspaces
Workspaces are the backbone of any monorepo. They tell your package manager where to find the individual packages and applications.
pnpm Workspace Configuration
If you chose pnpm, you will have a pnpm-workspace.yaml file at the root of your project:
packages:
- "apps/*"
- "packages/*"
This tells pnpm to treat every folder inside apps/ and packages/ as an individual workspace package.
npm or yarn Workspace Configuration
If you prefer npm or yarn, define workspaces in your root package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}
Adding a New Package
To add a new shared package, for example a utility library:
- Create the directory:
mkdir packages/utils - Initialize it with a
package.json:
{
"name": "@my-monorepo/utils",
"version": "0.0.1",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc",
"lint": "eslint src/"
},
"devDependencies": {
"typescript": "^5.4.0"
}
}
- Create your source file at
packages/utils/src/index.ts - Run
pnpm installfrom the root to link everything
Step 3: Dependency Management Across Packages
One of the biggest advantages of a monorepo Turborepo setup is sharing code between applications without publishing to npm. Here is how to reference internal packages.
Using Internal Packages as Dependencies
In your application’s package.json (e.g., apps/web/package.json), add the internal package:
{
"dependencies": {
"@my-monorepo/ui": "workspace:*",
"@my-monorepo/utils": "workspace:*"
}
}
The workspace:* protocol tells pnpm to resolve this dependency from the local workspace rather than the npm registry. After adding the dependency, run:
pnpm install
Now you can import directly from the shared package:
import { Button } from "@my-monorepo/ui";
import { formatDate } from "@my-monorepo/utils";
Managing External Dependencies
To add an external dependency to a specific workspace:
pnpm add axios --filter @my-monorepo/web
To add a dev dependency to the root (shared tooling):
pnpm add -Dw turbo
Pro tip: Keep shared dev dependencies like TypeScript, ESLint, and Prettier at the root level to ensure consistency across all packages.
Step 4: Configure Turborepo Pipelines
The turbo.json file is where you define your build pipelines. This is the heart of your Turborepo configuration and what makes the tool so powerful.
Basic turbo.json Configuration
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"]
}
}
}
Understanding the Pipeline Properties
| Property | Description |
|---|---|
dependsOn |
Tasks that must complete before this task runs. The ^ prefix means dependencies in other workspaces. |
outputs |
Files and directories to cache after a successful run. |
cache |
Set to false to disable caching (useful for dev servers). |
persistent |
Marks long-running tasks like dev servers that do not exit on their own. |
inputs |
Specifies which files Turborepo should consider when calculating cache keys. |
Running Tasks
With your pipeline defined, you can run tasks across all workspaces:
pnpm turbo build
pnpm turbo lint
pnpm turbo test
Turborepo automatically determines the correct execution order based on your dependency graph and runs independent tasks in parallel.
Step 5: Master Turborepo Caching Strategies
Caching is what makes Turborepo incredibly fast. When nothing has changed, Turborepo replays cached outputs instead of running the task again. Understanding caching is critical for an efficient monorepo Turborepo setup.
How Local Caching Works
- Turborepo creates a hash for each task based on source files, environment variables, and dependency outputs
- When a task completes, the outputs are stored in
node_modules/.cache/turbo - On subsequent runs, if the hash matches, Turborepo restores the cached output instantly
You will see this in your terminal output:
@my-monorepo/utils:build: cache hit, replaying logs
Fine-Tuning Cache Inputs
By default, Turborepo considers all files in a workspace when computing the cache hash. You can narrow this down for faster cache lookups:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": ["dist/**"]
}
}
}
Environment Variables and Caching
If your build depends on environment variables, declare them so the cache invalidates correctly:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["API_URL", "NODE_ENV"],
"outputs": ["dist/**"]
}
}
}
Remote Caching
Remote caching allows your entire team and CI servers to share a single cache. This means if a teammate already built a package with the same inputs, your machine gets the result instantly without rebuilding.
To enable remote caching with Vercel:
npx turbo login
npx turbo link
You can also self-host a remote cache server if your organization requires it. Several open-source implementations are available on GitHub.
Step 6: Set Up CI/CD Pipelines for Your Monorepo
A well-configured CI/CD pipeline is essential for a production-grade monorepo. Here is how to set up a GitHub Actions workflow optimized for Turborepo.
GitHub Actions Workflow
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Lint
run: pnpm turbo lint
- name: Test
run: pnpm turbo test
Key CI/CD Optimizations
- Remote caching in CI: Set
TURBO_TOKENandTURBO_TEAMas repository secrets so your CI pipeline benefits from the shared cache - Frozen lockfile: Always use
--frozen-lockfilein CI to ensure reproducible installs - Filtered runs: Use
turbo build --filter=@my-monorepo/web...to only build what is affected by the changes - Pruned deployments: Use
turbo pruneto create a minimal subset of your monorepo for Docker builds
Docker Deployment with turbo prune
For containerized deployments, turbo prune creates a stripped-down version of your monorepo containing only the target app and its dependencies:
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npx turbo prune @my-monorepo/web --docker
FROM node:20-alpine AS installer
WORKDIR /app
COPY --from=builder /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=builder /app/out/full/ .
RUN pnpm turbo build --filter=@my-monorepo/web
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=installer /app/apps/web/.next ./.next
COPY --from=installer /app/apps/web/public ./public
CMD ["node", "server.js"]
Step 7: Shared Configurations and Tooling
One of the biggest benefits of a monorepo is sharing configurations. Here is how to centralize your tooling.
Shared TypeScript Configuration
Create a base config at packages/typescript-config/base.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
Then extend it in each workspace’s tsconfig.json:
{
"extends": "@my-monorepo/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
Shared ESLint Configuration
Centralize your linting rules in packages/eslint-config and reference them from each workspace. This ensures consistent code quality across all your apps and packages.
Step 8: Scaling Your Monorepo Turborepo Setup
As your project grows, follow these best practices to keep your monorepo healthy.
Best Practices Checklist
- Keep packages focused: Each package should have a single, clear responsibility
- Use consistent naming: Prefix all internal packages (e.g.,
@my-monorepo/) - Avoid circular dependencies: Turborepo will warn you, but design your dependency graph carefully from the start
- Use Changesets for versioning: The Changesets tool works beautifully with Turborepo for managing package versions and changelogs
- Define clear ownership: Use CODEOWNERS files so the right people review the right packages
- Monitor task performance: Run
turbo build --summarizeto get detailed performance reports
Turborepo vs Nx: Quick Comparison
Since many developers evaluate both tools, here is a quick comparison to confirm you are making the right choice:
| Feature | Turborepo | Nx |
|---|---|---|
| Setup complexity | Minimal, convention-based | More configuration required |
| Learning curve | Low | Moderate to high |
| Caching | Local + Remote (Vercel or self-hosted) | Local + Nx Cloud |
| Best for | JS/TS projects, Next.js ecosystems | Large enterprise, multi-language |
| Maintained by | Vercel | Nrwl |
Turborepo is ideal if you want a lightweight, fast, and easy-to-adopt build system for your JavaScript and TypeScript monorepo. If you need extensive plugin ecosystems or work with multiple languages beyond JS/TS, Nx might be worth exploring.
Common Pitfalls to Avoid
- Not declaring all outputs: If you forget to list output directories in your pipeline, Turborepo cannot cache them. Always verify your
outputsarray. - Ignoring environment variables: Builds that depend on env vars will produce incorrect cache hits if the variables are not listed in the
envarray. - Running dev with caching enabled: Always set
"cache": falsefor dev tasks. Caching a dev server makes no sense and will cause confusing behavior. - Skipping the dependency graph: Run
turbo build --graphto visualize your dependency tree. This helps catch misconfigured dependencies early. - Monolithic shared packages: Resist the urge to dump everything into a single “shared” package. Split by concern (ui, utils, config, types).
Wrapping Up
Setting up a monorepo with Turborepo does not have to be complicated. With the right workspace structure, well-defined pipelines, smart caching strategies, and a solid CI/CD workflow, you can dramatically improve your team’s development velocity and code sharing.
The monorepo Turborepo setup we covered in this guide gives you a production-ready foundation. From here, you can extend it with Changesets for automated publishing, add more applications, and share even more configuration across your codebase.
At Box Software, we help teams architect and implement scalable monorepo solutions. If you need expert guidance for your monorepo migration or setup, do not hesitate to get in touch with our team.
FAQ
What is a monorepo?
A monorepo is a single repository that contains multiple projects, applications, or packages. Instead of maintaining separate repositories for each project, everything lives together, making it easier to share code, manage dependencies, and enforce consistent standards.
Does Turborepo handle package installation?
No. Turborepo is a build orchestration tool, not a package manager. You need to use pnpm, npm, or yarn to install dependencies. Turborepo works on top of your chosen package manager’s workspace feature.
Is Turborepo free to use?
Yes. Turborepo is open source and free. Remote caching through Vercel has a free tier with generous limits. You can also self-host your own remote cache server at no cost.
Can I add Turborepo to an existing project?
Absolutely. You can incrementally adopt Turborepo by adding a turbo.json to your existing workspace and defining pipelines for your tasks. You do not need to restructure your entire codebase at once.
Which package manager should I use with Turborepo?
pnpm is the most recommended option due to its strict dependency resolution and efficient disk usage. However, Turborepo works equally well with npm and yarn if your team already uses one of those.
How does Turborepo compare to Lerna?
Lerna is primarily a package publishing and versioning tool, while Turborepo focuses on build orchestration and caching. Many teams use Turborepo for builds alongside Changesets (a modern alternative to Lerna) for versioning and publishing.
Can I use Turborepo with Bun?
Yes. Turborepo has added support for Bun as a package manager. If you are using Bun in your projects, you can configure your Turborepo workspace to use Bun for dependency management and script execution.
