Blog
3 min read

pnpm Workspaces: Set Up a JavaScript Monorepo Step by Step

How to set up a monorepo with pnpm workspaces: pnpm-workspace.yaml, linking internal packages with workspace:*, running scripts across packages with --filter and -r, sharing dependency versions with catalogs, approving build scripts in pnpm 10, and deploying a single app from the repo.

A monorepo keeps several related projects — say a web app, an API and a shared UI library — in one repository. pnpm workspaces are one of the simplest ways to run one: fast installs, one lockfile, and internal packages linked together without publishing to npm. (Monorepo vs Polyrepo, npm vs pnpm vs Yarn vs Bun)

The layout

my-product/
├── pnpm-workspace.yaml
├── package.json
├── apps/
│   ├── web/        (Next.js or Vite)
│   └── api/        (Express, Fastify…)
└── packages/
    ├── ui/         (shared components)
    └── db/         (schema + queries)

Step 1: Declare the workspace

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

The root package.json is private and holds repo-wide scripts and dev tools:

{
  "name": "my-product",
  "private": true,
  "scripts": {
    "dev": "pnpm -r --parallel dev",
    "build": "pnpm -r build",
    "test": "pnpm -r test"
  },
  "packageManager": "pnpm@10.18.0"
}

packageManager pins the pnpm version for everyone through Corepack (set it to the version you actually use).

Give each package a name:

// packages/ui/package.json
{ "name": "@my/ui", "version": "0.0.0", "main": "src/index.ts" }

Depend on it from an app with the workspace: protocol:

// apps/web/package.json
{ "dependencies": { "@my/ui": "workspace:*" } }

Run pnpm install at the root. pnpm links packages/ui into apps/web/node_modules/@my/ui, so edits show up immediately. workspace:* guarantees you get the local copy, never a same-named package from npm.

Step 3: Run commands in the right place

pnpm --filter web dev               # one package (by name or folder)
pnpm --filter web add zod           # add a dependency to one package
pnpm --filter "./packages/**" build # a group of packages
pnpm --filter web... build          # web and everything it depends on
pnpm -r build                       # every package, in dependency order
pnpm add -D -w typescript           # a dev tool at the root (-w)

-r runs packages in topological order, so @my/ui builds before the apps that use it.

Step 4: One version of each dependency with catalogs

Monorepos drift: web on React 19.3, admin on 19.1. Catalogs define versions once:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
catalog:
  react: ^19.3.0
  zod: ^4.1.0
{ "dependencies": { "react": "catalog:" } }

Step 5: Approve build scripts (pnpm 10)

pnpm 10 doesn't run dependencies' install scripts (postinstall and friends) by default — a defence against supply-chain attacks. Packages that genuinely need them (native modules, Prisma, esbuild) must be allowed. Run:

pnpm approve-builds

and commit the resulting allowlist in your workspace config. (npm Supply Chain Security)

Step 6: Speed up builds (optional)

For a handful of packages, pnpm -r is enough. As the repo grows, a task runner like Turborepo or Nx caches build outputs and skips unchanged packages.

Deploying one app from the monorepo

Most hosts let you set a root directory (apps/web) and a build command. For Docker or a plain server, pnpm deploy creates a standalone folder with one package and only its production dependencies:

pnpm --filter api deploy --prod --legacy /tmp/api-out

For the pnpm 10 setup above, --legacy allows deployment without injected workspace dependencies. The default implementation requires injectWorkspacePackages: true; configure that deliberately if you prefer it. Build the app first and make sure its compiled output is included in the deployed package. See pnpm 10's deploy documentation.

(Production Dockerfile for Node)

TypeScript tips

Point packages at each other's source ("main": "src/index.ts") during development, or use TypeScript project references. Keep one tsconfig.base.json at the root and extend it in each package.

The summary

  • pnpm-workspace.yaml lists packages; workspace:* links them.
  • --filter and -r run commands in the right packages; catalogs keep versions aligned.
  • pnpm 10 blocks dependency build scripts until you approve them.

EasySpawn handles monorepos on a persistent server — Claude Code can run pnpm --filter builds and tests across packages and deploy just the app that changed. See how it works or join the waitlist.

Related: package-lock.json Explained · node_modules Explained · Semantic Versioning Explained · GitHub Actions CI Basics

Keep reading