<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/how-to-set-up-monorepo-turborepo-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/how-to-set-up-monorepo-turborepo-2026/raw.md -->
<!-- Source path: content/guides/how-to-set-up-monorepo-turborepo-2026.mdx -->

---
og_image: "/images/guides/how-to-set-up-monorepo-turborepo-2026.webp"
title: "How to Set Up a Monorepo with Turborepo in 2026"
description: "Complete guide to setting up a Turborepo monorepo from scratch. pnpm workspaces, task pipelines, remote caching, and a full Next.js + API example in 2026."
date: "2026-03-08"
author: "PkgPulse Team"
tier: 1
tags: ["turborepo", "monorepo", "pnpm", "nextjs", "2026"]
featured_comparison: "turborepo-vs-nx"
---

## TL;DR

**Turborepo + pnpm = the 2026 monorepo standard.** `create-turbo` scaffolds the whole thing in 30 seconds. This guide walks through a real setup: Next.js web app + Hono API + shared packages (UI, database, utils, config). Remote caching makes CI runs 80%+ faster after the first run.

## Key Takeaways

- **`npx create-turbo@latest`** — scaffold in 30 seconds
- **pnpm workspaces** — package manager layer (packages declare each other as dependencies)
- **turbo.json** — defines task dependencies and what to cache
- **Remote cache** — CI runs hit cache after the first build, 80%+ time savings
- **`workspace:*` protocol** — pnpm's way to reference internal packages

---

## Why Turborepo in 2026

Turborepo has become the default monorepo build tool for TypeScript applications for three reasons: minimal configuration, excellent Vercel integration (the Vercel acquisition brought first-party Next.js support), and a caching model that works correctly for the common case without requiring significant tuning.

A typical team encounters monorepo pain points before they encounter monorepo tooling. They start with multiple repositories, experience friction when shared code needs updating across all of them, and eventually consolidate into a monorepo. By the time they add Turborepo, the directory structure already exists — Turborepo adds the caching layer on top without requiring restructuring.

The concrete benefit of build caching is meaningful. In a monorepo with 10 packages, CI without caching rebuilds all 10 packages on every PR. With Turborepo's remote cache enabled, a PR that touches one package rebuilds only that package and its dependents — the rest serve cached artifacts from previous runs. Teams report 60-80% CI time reductions after enabling remote caching.

---

## Scaffold

```bash
npx create-turbo@latest my-monorepo
# Prompts: package manager (choose pnpm), app type
cd my-monorepo
pnpm install
```

The scaffold creates a working monorepo immediately. Start there rather than building from scratch.

---

## Project Structure

```
my-monorepo/
├── apps/
│   ├── web/           # Next.js 15 frontend
│   └── api/           # Hono API server
├── packages/
│   ├── ui/            # Shared React components
│   ├── database/      # Drizzle schema + db client
│   ├── utils/         # Shared TypeScript utilities
│   └── config/        # Shared tsconfig, biome config
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
```

The `apps/` vs `packages/` split is a convention, not a requirement. The distinction: `apps/` contains deployable applications that consumers use directly, `packages/` contains reusable libraries that `apps/` consume. Nothing prevents an `apps/` package from depending on another `apps/` package, but keeping `apps/` deployable and `packages/` reusable is a useful mental model.

The `packages/config` package is worth special mention. Sharing TypeScript configuration, ESLint rules, and Biome config through a workspace package ensures consistent tooling behavior across all packages. Without it, configuration drift between packages is a common maintenance burden.

---

## pnpm-workspace.yaml

```yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
```

---

## turbo.json

```json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**", ".svelte-kit/**"],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": [],
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": [],
      "cache": true
    },
    "clean": {
      "cache": false
    }
  },
  "remoteCache": {
    "enabled": true
  }
}
```

Understanding `dependsOn` is essential to getting Turborepo right. `"dependsOn": ["^build"]` means "before running `build` for this package, run `build` for all of its workspace dependencies first." The `^` prefix is workspace-scoped. `"dependsOn": ["build"]` (without `^`) means "run this package's own `build` task first" — useful for running `type-check` after `build` for the same package.

The `outputs` array tells Turborepo what to cache. Getting this right is the most common configuration mistake. If you list `.next/**` but your Next.js app also outputs to `.next/cache/**`, only the declared patterns are cached. Missing an output directory means that cached artifacts won't restore correctly, and you'll see mysterious "rebuilt from cache" builds that seem to work but actually re-executed.

`persistent: true` for the `dev` task tells Turborepo that this task runs indefinitely (it's a long-running dev server). Without this flag, Turborepo might wait for `dev` to complete before starting dependent tasks, which never happens.

---

## packages/config — Shared Configs

```json
// packages/config/package.json
{
  "name": "@myapp/config",
  "version": "0.0.0",
  "private": true,
  "files": ["biome.json", "tsconfig.base.json", "tsconfig.nextjs.json"]
}
```

```json
// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "skipLibCheck": true,
    "resolveJsonModule": true
  }
}
```

```json
// packages/config/tsconfig.nextjs.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "noEmit": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"]
  }
}
```

---

## packages/ui — Shared Components

```json
// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": { ".": "./src/index.ts" },
  "devDependencies": {
    "react": "^18.2.0",
    "typescript": "^5.3.0",
    "@myapp/config": "workspace:*"
  },
  "peerDependencies": {
    "react": "^18.2.0"
  }
}
```

```tsx
// packages/ui/src/index.ts
export { Button } from './button';
export { Card } from './card';
export type { ButtonProps } from './button';

// packages/ui/src/button.tsx
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'ghost';
  onClick?: () => void;
}

export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      className={`btn btn-${variant}`}
    >
      {children}
    </button>
  );
}
```

The `"main": "./src/index.ts"` approach (pointing directly to TypeScript source) works within the monorepo because consuming apps use TypeScript and compile everything at build time. For publishable packages, you'd compile to JavaScript first and point `main` to the compiled output. For internal packages that aren't published, the TypeScript source approach is simpler and provides better IDE experience.

---

## packages/database — Shared DB Client

```json
// packages/database/package.json
{
  "name": "@myapp/database",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "exports": { ".": "./src/index.ts" },
  "dependencies": {
    "drizzle-orm": "^0.30.0",
    "@neondatabase/serverless": "^0.9.0"
  }
}
```

```typescript
// packages/database/src/index.ts
export { db } from './client';
export * from './schema';

// packages/database/src/client.ts
import { drizzle } from 'drizzle-orm/neon-serverless';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
```

---

## apps/web — Next.js App

```json
// apps/web/package.json
{
  "name": "@myapp/web",
  "private": true,
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "@myapp/database": "workspace:*",
    "@myapp/utils": "workspace:*",
    "next": "15.0.0",
    "react": "18.3.0",
    "react-dom": "18.3.0"
  }
}
```

```json
// apps/web/tsconfig.json
{
  "extends": "@myapp/config/tsconfig.nextjs.json",
  "compilerOptions": {
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
```

---

## Root package.json Scripts

```json
// package.json (root)
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "test": "turbo test",
    "lint": "turbo lint",
    "type-check": "turbo type-check",
    "clean": "turbo clean && rm -rf node_modules"
  }
}
```

---

## Remote Cache Setup

```bash
# Connect to Vercel Remote Cache (free)
npx turbo login
npx turbo link  # Links to your Vercel team

# CI: set TURBO_TOKEN env var
# GitHub Actions example:
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

# CI result after first run:
# PR #1: full build → 4 minutes (populates cache)
# PR #2: cache hit on unchanged packages → 45 seconds
```

Remote caching requires a Vercel account but is free on the hobby plan. The `TURBO_TOKEN` is a personal access token from your Vercel account settings. The `TURBO_TEAM` is your Vercel team slug. With both set in GitHub Actions secrets and vars, every CI run automatically hits the remote cache.

For teams that can't use Vercel remote cache (self-hosted, enterprise requirements), `@turborepo/remote-cache` provides an open-source alternative that runs on S3, R2, or Minio.

---

## Common Pitfalls

**Phantom dependencies in strict mode:** pnpm's strict mode prevents importing packages not in your direct dependencies. After adding a package, you may discover other packages depended on it transitively. Fix by adding it explicitly to each package that needs it.

**TypeScript project references:** Large monorepos benefit from TypeScript project references, which enable incremental compilation across packages. Setting them up adds configuration but significantly improves IDE performance and `type-check` speed.

**Missing `outputs` in turbo.json:** If you add a new build artifact directory, add it to the `outputs` array. Otherwise, Turborepo won't cache it, and the cached "build" won't include the necessary files.

**Environment variables in CI:** Turborepo caches are keyed partly by environment variables declared in `globalEnv` or `env`. If your build behaves differently based on `NODE_ENV` but you haven't declared it in `globalEnv`, you may get stale cache hits with incorrect builds.

---

## Shared Package Architecture

The most valuable part of a monorepo setup isn't build caching — it's sharing code across packages without publishing to npm. Getting the shared package structure right from the start saves significant refactoring later.

**Internal Package Conventions**

Shared packages in a Turborepo monorepo live in `packages/` by convention. Each package has its own `package.json` with a name (typically scoped: `@acme/ui`, `@acme/utils`, `@acme/config`), TypeScript config, and build configuration. Apps in `apps/` reference internal packages using the `workspace:*` protocol in their `package.json` dependencies:

```json
{
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@acme/utils": "workspace:*"
  }
}
```

pnpm resolves `workspace:*` to the local package path at install time, creating a symlink. TypeScript resolves the package through the `paths` configuration in `tsconfig.json`, pointing to the source files directly for optimal IDE performance.

**Two Approaches to Internal Package Builds**

The first approach: build shared packages to JavaScript (`dist/`) before consuming apps use them. This is explicit and reliable — TypeScript type declarations are pre-generated, and apps import compiled output. The downside is the added build step: any change to a shared package requires rebuilding it before the consuming app reflects the change, even during development.

The second approach: configure consuming apps to compile shared package TypeScript directly. TypeScript's project references enable this — apps list their shared package dependencies in `tsconfig.json` references, and the compiler handles the cross-package compilation. This gives instant feedback during development (no rebuild step) but requires more TypeScript configuration and can slow down type-checking in very large repos.

Turborepo supports both approaches. For small-to-medium monorepos (under 20 packages), the direct compilation approach gives a better development experience. For larger repos, pre-built packages with Turborepo caching produce faster CI runs.

**Config Packages and Shared Tooling**

A pattern that prevents drift between packages is extracting shared configuration into dedicated packages: `@acme/eslint-config`, `@acme/tsconfig`, `@acme/prettier-config`. These packages contain only configuration files — no source code — and are referenced by other packages:

```json
// packages/web/tsconfig.json
{
  "extends": "@acme/tsconfig/nextjs.json",
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}
```

When your ESLint config needs updating, you update it in one place and all packages inherit the change after running `turbo lint`. Without this pattern, you'd update N separate config files and risk inconsistency.

## Remote Caching Setup for Teams

Remote caching is what makes Turborepo worth adopting for teams rather than individual developers. The setup is straightforward for Vercel-hosted projects and workable for other infrastructure.

**Vercel Remote Cache**

For projects deploying to Vercel, remote caching enables in three steps:

```bash
# 1. Install Turborepo globally (or use npx)
npm install -g turbo

# 2. Log in with Vercel
npx turbo login

# 3. Link your repo to a Vercel team
npx turbo link
```

After linking, CI runs that set `TURBO_TEAM` and `TURBO_TOKEN` environment variables share a cache with local development. A build that ran on a colleague's machine doesn't re-run in CI if the inputs are identical.

**Self-Hosted Remote Cache**

For teams not using Vercel, `turborepo-remote-cache` is an open-source Turborepo-compatible cache server. Deploy it to any Node.js-capable platform (Railway, Fly, a small EC2 instance) and configure the cache endpoint:

```bash
# turbo.json
{
  "remoteCache": {
    "enabled": true
  }
}

# Environment variables
TURBO_API="https://your-cache-server.example.com"
TURBO_TOKEN="your-secret-token"
TURBO_TEAM="your-team-slug"
```

The server stores artifacts in S3 or local disk. For a team of 5-10 engineers, a small Railway deployment with S3 artifact storage costs under $10/month and provides substantial CI time savings.

**Measuring Cache Effectiveness**

Turborepo outputs cache statistics at the end of each run. In CI, watch for the cache hit rate over time. A well-configured monorepo should see 60-80% cache hits after the first few weeks — full rebuilds happen only when dependencies change or environment variables are updated. If your hit rate is below 40%, the most common culprits are undeclared environment variables, broad `inputs` patterns matching too many files, or missing `outputs` declarations.

## Incremental Adoption: Adding Turborepo to an Existing Repo

Not all monorepo adoption starts from scratch. Adding Turborepo to an existing multi-package repository is often smoother than switching from a different monorepo tool.

The prerequisite is having pnpm workspaces (or npm/yarn workspaces) already configured. If you have a repository with multiple packages and a root-level workspace configuration, adding Turborepo is primarily a configuration exercise: create `turbo.json`, define your task pipeline, and run `turbo build` instead of your existing build script.

The first time you run Turborepo on an existing repository, it builds a cache from scratch by executing all tasks. Subsequent runs hit the cache for packages that haven't changed. The immediate visible benefit is on CI: the second CI run after adding Turborepo typically shows significant speedup as unchanged packages hit the cache.

Teams moving from Nx to Turborepo (or vice versa) face more complexity. Nx generators and plugins have no equivalents in Turborepo's leaner model, and task runner configuration in `project.json` (Nx) differs from `turbo.json`. The most practical migration approach is parallel operation: keep the existing tool for generated configuration and scaffolding, add Turborepo for task running and caching, then phase out the old tool's task running once the team is comfortable with the new setup.

The most important thing to get right early is the `outputs` configuration in `turbo.json`. Turborepo caches whatever you declare in `outputs` — if you miss a build artifact directory, the cache restores everything except that directory, leading to subtle failures where a "cached" build appears to succeed but is missing files. Run your full build pipeline on a clean checkout after configuring Turborepo to verify all outputs are captured correctly.

Package managers matter for Turborepo performance. pnpm's isolated node_modules and content-addressed store complement Turborepo's input hashing. Bun's faster install time pairs well with Turborepo's caching — Bun installs in seconds, Turborepo avoids running tasks for unchanged packages. npm workspaces with hoisted node_modules work correctly with Turborepo but provide less isolation and slower install times at scale.

## Quick Reference: Common Configuration Pitfalls

Teams new to Turborepo consistently encounter the same configuration issues. Knowing them in advance prevents hours of debugging.

**Cache misses after changing non-code files**: If you modify `package.json` (not just the scripts section, but adding or removing dependencies), Turborepo invalidates the cache for that package and all packages that depend on it. This is correct behavior — dependency changes can affect build output. If your cache hit rate drops unexpectedly, check whether a recent `package.json` change is the cause before investigating more complex issues.

**`outputs` not including all generated files**: A common mistake is declaring `outputs: ["dist/**"]` when the actual build output includes type definitions at `dist/**` and source maps at `.tsbuildinfo`. Without the source maps in `outputs`, they aren't cached and won't be restored on cache hits. Use `outputs: ["dist/**", "*.tsbuildinfo"]` to capture all artifacts.

**Missing `dependsOn` for cross-package builds**: If package A depends on package B's types, and you run `build` in parallel across all packages, A may start building before B's type definitions are available. The `"dependsOn": ["^build"]` pattern (where `^` means "in dependency packages first") ensures topological build ordering. For most JavaScript monorepos, this configuration should be the default for the `build` task.

**Global `turbo.json` vs package-level overrides**: Turborepo supports per-package task configurations in each `package.json`'s `turbo` field, which override the root `turbo.json`. This is useful when one package has unique build requirements, but inconsistent use of this feature creates surprising cache behavior. Prefer keeping task configuration centralized in the root `turbo.json` when possible.

*Compare monorepo tooling on [PkgPulse](/compare/turborepo-vs-nx). Related: [Best Monorepo Tools 2026](/guides/best-monorepo-tools-2026), [Best JavaScript Package Managers 2026](/guides/best-javascript-package-managers-2026), and [Best JavaScript Testing Frameworks 2026](/guides/best-javascript-testing-frameworks-2026).*
