What You'll Build

In this tutorial, you'll set up a monorepo with three packages using pnpm Workspaces: a shared utility library, a backend API, and a frontend app. By the end, you'll understand how to install dependencies, share internal packages, and run scripts across the entire workspace.

Prerequisites

  • Node.js 18 or later installed
  • pnpm installed globally: npm install -g pnpm
  • Basic familiarity with JavaScript/TypeScript projects

Step 1 — Initialize the Root Workspace

Create a new directory and initialize it as a pnpm workspace:

mkdir my-monorepo && cd my-monorepo
pnpm init

Edit the generated package.json to add a private: true field (the root package should never be published):

{
  "name": "my-monorepo",
  "private": true,
  "version": "0.0.1"
}

Step 2 — Create the Workspace Configuration

Create a pnpm-workspace.yaml file in the root to tell pnpm where your packages live:

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

This tells pnpm to treat every subdirectory under packages/ and apps/ as a workspace package.

Step 3 — Create Your Packages

Set up the directory structure:

mkdir -p packages/utils apps/api apps/web

Initialize each package with its own package.json:

# packages/utils/package.json
{
  "name": "@my-monorepo/utils",
  "version": "1.0.0",
  "main": "./src/index.js"
}

# apps/api/package.json
{
  "name": "@my-monorepo/api",
  "version": "1.0.0",
  "dependencies": {
    "@my-monorepo/utils": "workspace:*"
  }
}

# apps/web/package.json
{
  "name": "@my-monorepo/web",
  "version": "1.0.0",
  "dependencies": {
    "@my-monorepo/utils": "workspace:*"
  }
}

The workspace:* protocol is pnpm's way of declaring a dependency on another package within the same workspace. It resolves to the local package rather than fetching from the registry.

Step 4 — Install All Dependencies

From the root of the monorepo, run:

pnpm install

pnpm will install all dependencies for all packages in one pass, creating symlinks in each package's node_modules that point to the local workspace packages. This means changes to packages/utils are immediately reflected in apps/api and apps/web without any rebuild step.

Step 5 — Add Dependencies to Specific Packages

To add a dependency to a specific workspace package, use the --filter flag:

# Add express to the api app only
pnpm --filter @my-monorepo/api add express

# Add a dev dependency to the root workspace (shared tools like eslint)
pnpm add -D eslint -w

The -w flag explicitly targets the root workspace, which is required when private: true is set.

Step 6 — Run Scripts Across All Packages

Add scripts to each package's package.json, then use pnpm -r (recursive) to run them across the entire monorepo:

# Run the build script in all packages
pnpm -r run build

# Run tests only in the apps/* packages
pnpm --filter './apps/**' run test

# Run a script in a specific package
pnpm --filter @my-monorepo/api run start

Step 7 — Understanding the node_modules Structure

Unlike npm's nested approach, pnpm uses a content-addressable store and hard-links packages from a global cache. Each workspace package gets a node_modules with symlinks to the store, dramatically reducing disk usage when multiple packages share the same dependencies.

Recap: Key pnpm Workspace Commands

CommandEffect
pnpm installInstall all workspace dependencies
pnpm --filter <name> add <pkg>Add dependency to a specific package
pnpm -r run <script>Run script in all packages
pnpm --filter <pattern> run <script>Run script in matched packages
pnpm add -D <pkg> -wAdd dev dependency to root

pnpm Workspaces strike an excellent balance between simplicity and power for monorepo management. The workspace:* protocol and the filter system make it easy to reason about your dependency graph while keeping the tooling fast and the disk footprint small.