Skip to content
Dev Tools Intermediate Tutorial

Build a JavaScript Monorepo with Turborepo and pnpm Workspaces

Wire up a pnpm workspace monorepo with Turborepo task orchestration and local caching — then extend it with a shared utility package and optional remote cache for CI.

Mariana Souza
Mariana Souza
Senior Editor · Jun 12, 2026 · 7 min read

What You'll Build

A pnpm workspace monorepo with Turborepo orchestrating builds across two Next.js apps and a shared TypeScript config package. After the first full build, re-running against unchanged code completes in milliseconds via local cache — remote caching extends that to every CI machine and teammate.

Prerequisites

  • Node.js 18 or 20 LTS — verify with node --version
  • pnpm 9+: corepack enable && corepack prepare pnpm@latest --activate
  • A free Vercel account for remote caching (optional; covered in step 6)
  • Comfort with TypeScript and how npm packages resolve

1. Scaffold with create-turbo

pnpm dlx create-turbo@latest my-monorepo
cd my-monorepo
pnpm install

When prompted, select pnpm as the package manager. The resulting layout:

my-monorepo/
├── apps/
│   ├── web/                  # Next.js app
│   └── docs/                 # Next.js docs site
├── packages/
│   ├── ui/                   # Shared React components
│   ├── eslint-config/        # Shared ESLint rules
│   └── typescript-config/    # Shared tsconfig bases
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

2. Understand the Workspace Wiring

pnpm-workspace.yaml tells pnpm which directories are packages:

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

Internal packages reference each other with the workspace protocol so pnpm never hits the registry for them:

// apps/web/package.json (excerpt)
{
  "dependencies": {
    "@repo/ui": "workspace:*"
  }
}

workspace:* always resolves to the local version — never a published release.

3. Review turbo.json

Turborepo 2.x uses a tasks key (the older pipeline key is removed):

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
Key Effect
"^build" Run build in all dependency packages before this one
outputs Files Turbo stores and restores on a cache hit
"cache": false Dev servers produce no cacheable artifact
"persistent": true Task never exits; Turbo won't block the rest of the pipeline

4. Add a Shared Utility Package

mkdir -p packages/utils/src

packages/utils/package.json:

{
  "name": "@repo/utils",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "typescript": "^5.0.0"
  }
}

packages/utils/tsconfig.json:

{
  "extends": "@repo/typescript-config/base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

packages/utils/src/index.ts:

export function formatDate(date: Date): string {
  return date.toISOString().split("T")[0];
}

Add the package to apps/web, then use it. Quote the version specifier to prevent shell glob expansion of *:

Advertisement
pnpm --filter web add '@repo/utils@workspace:*'
// apps/web/app/page.tsx
import { formatDate } from "@repo/utils";

export default function Page() {
  return <p>Today: {formatDate(new Date())}</p>;
}

5. Run Your First Build

pnpm exec turbo run build

Turbo resolves the dependency graph automatically: typescript-config and utils compile before web and docs. The first run is a cache miss for every task. Run it again immediately:

pnpm exec turbo run build

Every task now shows cache hit, replaying output. The full build finishes in milliseconds.

6. Enable Remote Caching

Remote caching shares the cache across your team and CI. Using Turbo's Vercel-hosted cache:

pnpm exec turbo login
pnpm exec turbo link

turbo login stores your auth token globally in ~/.turbo/config.json. turbo link writes the linked team and project to the repo-local .turbo/config.json (already git-ignored by the scaffold). In CI, supply the same cache via environment variables instead:

# .github/workflows/ci.yml (excerpt)
- name: Build
  run: pnpm exec turbo run build
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ vars.TURBO_TEAM }}

TURBO_TOKEN is a personal access token from vercel.com/account/tokens. TURBO_TEAM is your Vercel team slug (e.g., acme-corp). Turbo detects both variables automatically — no other config changes needed.

Verify It Works

pnpm exec turbo run build --dry=json

--dry=json prints the resolved task graph without executing anything. Confirm that @repo/utils appears as a dependency of web in the tasks array. A successful real build ends with output similar to:

Tasks:    6 successful, 6 total
Cached:   6 cached, 6 total
  Time:   289ms >>> FULL TURBO

FULL TURBO confirms every task was a cache hit.

Troubleshooting

ERR_PNPM_WORKSPACE_PKG_NOT_FOUND — A workspace:* dependency can't resolve. Verify the name field in the target's package.json matches the import exactly, then re-run pnpm install.

Cache never hits on the second run — The outputs glob doesn't cover what your build actually writes. Double-check that paths match real artifacts (dist/** for TypeScript packages, .next/** for Next.js apps). Output paths not listed in outputs won't be saved to or restored from the cache, so Turbo will re-run the task even when inputs haven't changed.

turbo: command not found in CIturbo must be a root devDependency. Run pnpm add -Dw turbo and commit the updated lockfile. Always invoke it via pnpm exec turbo rather than relying on a globally installed binary.

Next.js build cache inflating artifacts — The !.next/cache/** exclusion in outputs is intentional. Next.js manages that directory internally; including it causes cache bloat and slower restores.

Next Steps

  • Filter builds for deployment: pnpm exec turbo run build --filter=web... builds only web and the packages it depends on — ideal for single-app deploy steps in CI. (The trailing ... means "this package and its transitive dependencies"; the leading form ...web means dependents of web, which is the opposite.)
  • Versioning and publishing: Add Changesets to manage semver and changelogs when packages inside the repo are published publicly.
  • Scaffold new packages consistently: pnpm exec turbo gen creates apps or packages from Plop-based templates defined in turbo/generators/config.ts.
  • Self-hosted remote cache: The Turborepo Remote Cache API is an open HTTP spec; open-source implementations back it with S3, R2, or GCS if Vercel's cloud cache doesn't meet your compliance requirements.
Mariana Souza
Written by
Mariana Souza · Senior Editor

Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading