Skip to content

Turborepo + pnpm Workspaces: The Standard Approach to Monorepos

Mar 27, 2026 1 min
TL;DR Turborepo solves monorepo build speed problems; pnpm workspaces solves dependency sharing. Together they are the best choice for JS/TS monorepos today.
Table of Contents
  1. What are pnpm Workspaces
  2. What is Turborepo
  3. Caching
  4. Usage in DaoDao and NobodyClimb
  5. Common Commands
  6. When Is It Worth Adopting
  7. Trade-offs
  8. References

🌏 中文版

The problem with monorepos isn't "putting multiple projects in one place" — it's that once you do, builds get slow and dependencies become a mess. Turborepo solves build efficiency; pnpm workspaces solves dependency management. Together they are the standard answer for JS/TS monorepos today.

What are pnpm Workspaces

pnpm's workspaces feature lets you manage multiple packages in a single repo, sharing a node_modules directory so you don't need to install the same dependencies over and over.

Place a pnpm-workspace.yaml in the repo root:

packages:
  - "apps/*"
  - "packages/*"

Then a single pnpm install installs all dependencies across every package. Packages can reference each other:

// apps/product/package.json
{
  "dependencies": {
    "@myproject/ui": "workspace:*",
    "@myproject/shared": "workspace:*"
  }
}

workspace:* means resolve from the local packages/ui and packages/shared inside the monorepo, not from the npm registry.

What is Turborepo

Turborepo is a build orchestration tool for monorepos. It does two things:

  1. Task dependency management: defines which tasks must finish first and which can run in parallel
  2. Output caching: if inputs haven't changed, reuse the previous output instead of re-running the task

Configure the pipeline in turbo.json:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "lint": {
      "dependsOn": []
    },
    "type-check": {
      "dependsOn": ["^build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

The ^ in ^build means "run the dependency package's build first." Since apps/product depends on packages/ui, Turborepo ensures packages/ui builds before apps/product starts.

Caching

Turborepo uses a hash of inputs as the cache key:

  • Inputs: source files, environment variables, turbo.json configuration
  • Outputs: build artifacts (.next/, dist/)

First build:

$ pnpm turbo build
• Packages in scope: website, product, ui, shared
• Running build in 4 packages
Tasks:    4 successful, 4 total
Cached:   0 cached, 4 total
Time:     45.2s

Second build (no files changed):

$ pnpm turbo build
Tasks:    4 successful, 4 total
Cached:   4 cached, 4 total
Time:     312ms

45 seconds down to 312ms — because everything was a cache hit. On CI, you can configure Remote Cache (Turborepo's cloud cache or self-hosted) so different machines share build caches.

Usage in DaoDao and NobodyClimb

Both projects use a similar monorepo structure:

DaoDao:

apps/
  website/     # Marketing site (Next.js)
  product/     # Main application (Next.js)
  mobile/      # Expo / React Native
packages/
  shared/      # Shared types, utils
  ui/          # shadcn/ui component library
  i18n/        # Internationalization
  api/         # OpenAPI client

NobodyClimb:

apps/
  web/         # Next.js 15
  mobile/      # React Native + Expo
packages/
  schemas/     # Zod schemas (shared between frontend and backend)
  api-client/  # API client

The common pattern: everything in packages/ must build before apps/ can build. Turborepo's ^build handles this ordering automatically — no manual coordination needed.

NobodyClimb also places a Hono backend in backend/, managed through the same pnpm workspaces. Types are shared from packages/schemas — the request body the frontend sends and the schema the backend validates against are the same Zod definition, eliminating frontend/backend type mismatches entirely.

Common Commands

# Build all packages
pnpm turbo build

# Build only a specific app
pnpm turbo build --filter=product

# Build only affected packages (compared to main branch)
pnpm turbo build --filter=...[main]

# Dev mode, start all apps simultaneously
pnpm turbo dev

# Run a command in a specific workspace
pnpm --filter product add react-query

--filter is the most frequently used flag in monorepos — it lets you run tasks only on the packages you care about.

When Is It Worth Adopting

Monorepo is not the default choice. It makes sense when:

  • Multiple apps share code: UI component libraries, type definitions, utils, API clients
  • Frontend and backend share types: Zod schemas serving as both request/response validation and type sources
  • You want a unified lint / type-check / test pipeline: one command runs across the entire repo

It's not the right fit when:

  • You only have one app with no real code-sharing needs
  • The team isn't familiar with workspace link mechanics, making debugging costly
  • The repo mixes wildly different tech stacks (e.g., Python and JS together)

Trade-offs

Benefits:

  • Code sharing becomes straightforward; type safety extends across packages
  • Build caching significantly reduces CI time
  • Unified toolchain (lint, format, type-check)

Drawbacks:

  • Initial setup has a learning curve; workspace link resolution can be unintuitive to debug
  • Remote Cache requires extra configuration (Vercel's offering is a commercial service)
  • pnpm install on large monorepos is still not fast

For multi-app projects like DaoDao and NobodyClimb, Turborepo + pnpm workspaces is a worthwhile investment — the development experience is significantly better than maintaining multiple separate repos.

References