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
| Command | Effect |
|---|---|
pnpm install | Install 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> -w | Add 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.