March 26, 20268 min read

Monorepo Guide — Turborepo, Nx, and pnpm Workspaces

Set up a monorepo with Turborepo, Nx, or pnpm workspaces. Task orchestration, dependency management, CI optimization, and when a monorepo actually makes sense.

monorepo turborepo nx pnpm architecture
Ad 336x280

A monorepo is not "putting all your code in one repository." That's just a big repo. A monorepo is a repository containing multiple distinct projects that share tooling, dependencies, and build infrastructure. Google, Meta, Microsoft, and Uber all use monorepos — but they also have dedicated teams maintaining their monorepo tooling.

For the rest of us, the question is whether the benefits outweigh the tooling overhead. This guide covers when monorepos make sense, how to set one up with three popular tools, and the pitfalls that make teams regret the decision.

Should You Use a Monorepo?

Monorepo makes sense when...Polyrepo is better when...
Multiple packages share types/interfacesTeams are independent with different deploy cycles
You want atomic commits across packagesPackages have wildly different tech stacks
Shared component library + multiple appsYou don't want to maintain monorepo tooling
Team is small to medium (2-30 devs)Hundreds of developers (without dedicated infra team)
Frontend + backend + shared typesOpen source libraries with independent versioning
The biggest benefit: atomic changes. If you update a shared type, the consuming apps break in the same PR. You catch the breakage before merge, not in a separate deploy three days later.

The biggest cost: tooling complexity. You need task orchestration, smart caching, and CI that doesn't rebuild everything on every commit.

Tool Comparison

Featurepnpm WorkspacesTurborepoNx
What it isPackage managerBuild orchestratorFull build system
Task runningBasic (--filter)Parallel, cachedParallel, cached, distributed
Remote cacheNoYes (Vercel)Yes (Nx Cloud)
Dependency graphImplicitExplicit (turbo.json)Auto-detected
Code generationNoNoYes (generators)
Learning curveLowLow-MediumMedium-High
Best forSimple monoreposFrontend-heavy monoreposLarge, complex monorepos
You can combine them: pnpm workspaces for dependency management + Turborepo for build orchestration is a popular combination.

Option 1: pnpm Workspaces (Foundation)

pnpm workspaces is the lightest approach. No extra tool, just your package manager.

Setup

# Install pnpm globally
npm install -g pnpm

# Initialize the monorepo
mkdir my-monorepo && cd my-monorepo
pnpm init

Create the workspace configuration:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev": "pnpm --filter './apps/*' dev",
    "build": "pnpm --filter './apps/*' build",
    "lint": "pnpm -r lint",
    "test": "pnpm -r test"
  }
}

Creating Packages

mkdir -p packages/shared apps/web apps/api
// packages/shared/package.json
{
  "name": "@myorg/shared",
  "version": "0.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}
// packages/shared/src/index.ts
export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(date);
}

export const API_ENDPOINTS = {
users: "/api/users",
products: "/api/products",
orders: "/api/orders",
} as const;

// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/shared": "workspace:*"
  }
}
# Install dependencies — pnpm links workspace packages automatically
pnpm install

Now apps/web can import from @myorg/shared:

import { User, formatDate, API_ENDPOINTS } from "@myorg/shared";

Running Scripts

# Run dev in all apps
pnpm -r dev

# Run build only in web app
pnpm --filter @myorg/web build

# Run build in web and all its dependencies
pnpm --filter @myorg/web... build

# Run tests in packages that changed since main
pnpm --filter "...[origin/main]" test

Option 2: Turborepo (Build Orchestration)

Turborepo adds smart task scheduling and caching on top of your package manager. It understands your dependency graph and only rebuilds what changed.

Setup

# Add Turborepo to existing pnpm workspace
pnpm add -D turbo --workspace-root

# Or create a new project
npx create-turbo@latest
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/", "dist/"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {},
    "test": {
      "dependsOn": ["build"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    }
  }
}

Key concepts in turbo.json:

  • dependsOn: ["^build"] — Run build in dependencies first (the ^ means "upstream packages")
  • outputs — Files that Turborepo caches. On a cache hit, it restores these instead of running the task
  • cache: false — Don't cache dev servers (they're long-running)
  • persistent: true — This task runs indefinitely (dev servers)

Task Running

# Build everything in dependency order
turbo build

# Build only web and its dependencies
turbo build --filter=@myorg/web...

# Run dev servers for all apps in parallel
turbo dev

# See what would run without actually running
turbo build --dry-run

Caching

Turborepo's cache is its killer feature. After the first build, subsequent builds of unchanged packages are instant — Turborepo replays the cached output instead of running the build command.

$ turbo build
 Tasks:    3 successful, 3 total
 Cached:   2 cached, 3 total       # 2 out of 3 packages were cached
 Time:     1.2s                     # Instead of 45s

Remote caching with Vercel:

npx turbo login
npx turbo link

Now cache is shared across your team and CI. Developer A builds the shared package, developer B gets the cached result without building locally.

Option 3: Nx (Full Build System)

Nx is the most feature-rich option. It auto-detects your dependency graph, generates code, and supports distributed task execution.

Setup

# Create new Nx workspace
npx create-nx-workspace@latest my-workspace

# Or add to existing repo
npx nx@latest init
// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "lint": {
      "cache": true
    },
    "test": {
      "cache": true
    }
  },
  "defaultBase": "main"
}

Generating Projects

Nx has generators for common project types:

# Generate a React app
nx generate @nx/react:app web

# Generate a Node.js library
nx generate @nx/node:library shared-utils

# Generate a component
nx generate @nx/react:component Button --project=web

Affected Commands

Nx knows which projects are affected by a change:

# Only build projects affected by changes since main
nx affected -t build

# Only test affected projects
nx affected -t test

# See the dependency graph in your browser
nx graph

Project Configuration

// apps/web/project.json
{
  "name": "web",
  "targets": {
    "build": {
      "executor": "@nx/next:build",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/web"
      }
    },
    "dev": {
      "executor": "@nx/next:server",
      "options": {
        "port": 3000
      }
    }
  }
}

Shared Configuration

One of the biggest wins of a monorepo: shared configuration files.

Shared TypeScript Config

// tsconfig.base.json (root)
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "moduleResolution": "bundler",
    "paths": {
      "@myorg/shared": ["packages/shared/src"],
      "@myorg/ui": ["packages/ui/src"]
    }
  }
}
// apps/web/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "es2022"],
    "module": "esnext",
    "target": "es2017"
  },
  "include": ["src/*/", "next-env.d.ts"],
  "exclude": ["node_modules"]
}

Shared ESLint Config

// packages/eslint-config/index.js
module.exports = {
  extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  parser: "@typescript-eslint/parser",
  rules: {
    "no-console": "warn",
    "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
  },
};
// apps/web/.eslintrc.js
module.exports = {
  extends: ["@myorg/eslint-config"],
  // App-specific overrides
};

CI/CD Optimization

A naive CI pipeline rebuilds everything on every commit. That's slow and wasteful. Use affected/changed detection:

GitHub Actions with Turborepo

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for affected detection

- uses: pnpm/action-setup@v4
with:
version: 9

- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- run: pnpm install --frozen-lockfile

- run: turbo build lint test --filter="...[origin/main]"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

The --filter="...[origin/main]" flag tells Turborepo to only run tasks for packages that changed since the main branch. Combined with remote caching, CI for a large monorepo can finish in under a minute.

Common Mistakes

  1. Starting with a monorepo for a solo project. A monorepo's benefits come from shared code and coordinated deployments. If you have one app and no shared packages, it's overhead for no gain.
  1. Circular dependencies between packages. Package A imports from B, B imports from A. This breaks build order. Refactor the shared code into a third package.
  1. Not pinning workspace dependency versions. Use workspace:* in pnpm to always use the local version. Don't use semver ranges for workspace packages.
  1. Forgetting --frozen-lockfile in CI. Without it, CI might install different dependency versions than your local environment, causing phantom build failures.
  1. Building everything on every PR. Without affected detection or caching, your 20-minute build time will kill developer productivity.
Monorepo architecture is about developer experience at scale. If it's slowing you down instead of speeding you up, you've gone wrong somewhere. For more on project architecture and build tooling, check out the exercises on CodeUp.
Ad 728x90