Skip to main content

React Monorepo Setup with Turborepo and pnpm

A React monorepo using Turborepo and pnpm gives your team a single source of truth for all application code, shared libraries, and configuration. Turborepo orchestrates builds and tests across all packages with intelligent caching; pnpm eliminates dependency duplication with phantom dependencies and a flat node_modules structure. This article walks you through initializing a monorepo from scratch, configuring pnpm workspaces, and running your first cross-package build task in under 20 minutes.

Prerequisites and Environment​

Before you start, ensure you have Node.js 18+ and pnpm 8+ installed on your machine. You can verify your versions with node --version and pnpm --version. Familiarity with React, npm/yarn, and basic terminal commands is assumed. You will need a clean directory and git initialized for version control.

Creating a New Monorepo Structure​

Start by creating a root directory for your monorepo and initialize git and pnpm workspaces:

mkdir my-react-monorepo
cd my-react-monorepo
git init
pnpm init

Edit the root package.json to add workspace definitions. The pnpm-workspace.yaml file is the traditional approach, but you can also use the workspaces array in package.json. Create pnpm-workspace.yaml:

packages:
- 'apps/*'
- 'packages/*'

This tells pnpm that all subdirectories under apps/ and packages/ are workspaces. Create these directory structures:

mkdir -p apps packages

Installing Turborepo and Initial Setup​

Turborepo is installed as a dev dependency at the root workspace and orchestrates tasks across all packages. Install it with pnpm at the monorepo root:

pnpm add -D -w turbo

The -w flag installs the package in the workspace root, not a subdirectory. Next, create a turbo.json file at the root to define your task graph:

{
"$schema": "https://turbo.build/schema.json",
"version": "2",
"tasks": {
"dev": {
"cache": false,
"interactive": true,
"persistent": true
},
"build": {
"outputs": ["dist/**"],
"cache": true
},
"test": {
"cache": true,
"outputs": [".coverage/**"]
},
"lint": {
"cache": true
}
}
}

This turbo.json defines four tasks: dev (interactive, no caching), build (cache outputs in dist/), test, and lint. Turborepo will run these commands across all packages that define them.

Creating Your First Application and Library Packages​

Create a simple React app workspace under apps/. Use Vite for fast builds:

pnpm create vite apps/web -- --template react-ts
cd apps/web
pnpm install

Then create a shared library workspace for reusable components:

mkdir -p packages/ui-components
cd packages/ui-components
pnpm init

Edit packages/ui-components/package.json to add build and export fields:

{
"name": "@myapp/ui-components",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js",
"./button": "./dist/button.js"
},
"scripts": {
"build": "tsc && cp src/index.ts dist/index.js",
"dev": "tsc --watch"
},
"dependencies": {
"react": "workspace:*"
},
"devDependencies": {
"typescript": "workspace:*"
}
}

The "workspace:*" protocol tells pnpm to link these dependencies to the workspace version, not fetch from npm.

Linking Packages and Running Tasks​

After creating your first packages, install dependencies at the root:

cd /path/to/my-react-monorepo
pnpm install

pnpm reads pnpm-workspace.yaml, symlinks each package into node_modules, and installs all dependencies. Now you can make the web app depend on the ui-components library. Edit apps/web/package.json:

{
"dependencies": {
"@myapp/ui-components": "workspace:*"
}
}

Then run pnpm install again to establish the link. You can now import components:

import Button from '@myapp/ui-components/button';

Running Turborepo Tasks Across All Packages​

With packages configured, run Turborepo commands to build and develop across your entire monorepo:

# Run dev mode in all packages (non-blocking)
turbo dev

# Build all packages and cache outputs
turbo build

# Build only the web app and its dependencies
turbo build --filter=web

# Run tests in all packages
turbo test

Turborepo automatically determines the dependency graph. When you build web, Turborepo detects that it depends on @myapp/ui-components, builds that first, and caches the outputs so subsequent builds are instant.

Verifying Your Monorepo Setup​

Open the web app and verify it loads:

cd apps/web
pnpm run dev

Your Vite dev server starts on http://localhost:5173. If you can see the Vite welcome page, your setup is working. To confirm workspace linking, add a log statement to your ui-components library, rebuild, and check the web app imports it correctly.

Key Takeaways​

  • Use pnpm-workspace.yaml to declare all workspaces under apps/ and packages/ directories.
  • Install Turborepo at the root with pnpm add -D -w turbo and define tasks in turbo.json.
  • Use "workspace:*" in dependency versions to link packages within the monorepo, not fetch from npm.
  • Run cross-workspace commands like turbo build and turbo dev from the root to orchestrate all packages.
  • Turborepo caches task outputs automatically, making incremental builds nearly instant.

Frequently Asked Questions​

Can I use npm or yarn instead of pnpm?​

Yes, both npm and yarn support workspaces. However, pnpm is faster and uses less disk space because it deduplicates dependencies. npm 9+ and yarn 4+ have comparable features, but pnpm is the standard for new monorepos in 2026.

What is the difference between turbo build and turbo dev?​

turbo build compiles packages and caches outputs for reuse in CI/CD. turbo dev runs dev servers and watchers with caching disabled and in interactive mode, so changes reload immediately.

How does Turborepo know the order to build packages?​

Turborepo reads the dependencies and devDependencies in each package.json. If app A depends on lib B, Turborepo builds B first, then A. This is the task graph.

Can I run a task in only one package?​

Yes, use --filter. For example, turbo build --filter=web builds only the web app and its dependencies. Use --filter=@myapp/ui-components to target a package by its full npm name.

What is the workspace:* protocol?​

"workspace:*" is a pnpm special syntax that tells it to use the local workspace version of a dependency instead of fetching from npm. At publish time, workspace:* is converted to a real version number.

Further Reading​