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