4 min read
How to setup a monorepo using pnpm workspaces

Setting Up a Monorepo with pnpm Workspaces

I love pnpm workspaces, It makes code sharing so easy and less painful. Here’s how to set one up step by step.

Why pnpm?

You might be wondering why pnpm instead of npm or yarn. The main advantages are:

  • Disk space efficiency: pnpm uses a content-addressable store for dependencies, so you don’t end up duplicating packages across projects.
  • Speed: With its symlink-based approach, installing dependencies is often faster.
  • Workspaces support: pnpm makes managing multiple packages in a monorepo simple and clean.

Step 1: Create Your Monorepo Folder

First, create a folder for your monorepo and initialize it:

mkdir my-monorepo
cd my-monorepo
pnpm init

This will create a package.json at the root, which will hold shared dependencies and scripts.

Step 2: Configure pnpm Workspaces

Create a pnpm-workspace.yaml file in your root directory:

packages:
  - "packages/*"

This tells pnpm to treat every folder under packages as a separate workspace package. The pnpm-workspace.yaml file keeps workspace configuration clean and separate from your package metadata.

Step 3: Create Your Packages

Now, let’s create a couple of example packages:

mkdir -p packages/app
mkdir -p packages/ui

Inside each folder, initialize a new package:

cd packages/app
pnpm init
cd ../ui
pnpm init

At this point, your folder structure should look like this:

my-monorepo/
├─ package.json
├─ pnpm-workspace.yaml
└─ packages/
   ├─ app/
   │  └─ package.json
   └─ ui/
      └─ package.json

One of the perks of a monorepo is that you can use your own packages as dependencies without publishing them.

For example, if app needs ui, open packages/app/package.json and add:

{
  "dependencies": {
    "ui": "workspace:*"
  }
}

Then run:

pnpm install

pnpm will automatically symlink the ui package into app, so you can import it just like any other npm package.

Step 5: Adding Dependencies to Specific Packages

When working with workspaces, you’ll often need to add dependencies to specific packages. pnpm’s --filter flag makes this easy:

# Add a dependency to a specific package
pnpm add react --filter app

# Add a dev dependency
pnpm add -D typescript --filter ui

# Add to multiple packages
pnpm add lodash --filter "app" --filter "ui"

# Run scripts for specific packages
pnpm run build --filter ui
pnpm run test --filter "!app"  # Run tests for all packages except app

Best Practices

Manage Shared Dependencies at Root Level

For dependencies used across multiple packages (like TypeScript, ESLint), install them at the root:

pnpm add -D typescript eslint --workspace-root

Use Consistent Naming

Consider prefixing your packages with a scope:

// packages/ui/package.json
{
  "name": "@mycompany/ui"
}

// packages/app/package.json
{
  "name": "@mycompany/app"
}

Troubleshooting

Peer Dependency Issues

If you encounter peer dependency warnings, you can use .pnpmrc to configure hoisting:

hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*

TypeScript Path Mapping

For TypeScript projects, update your tsconfig.json to resolve workspace packages:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@mycompany/*": ["packages/*/src"]
    }
  }
}

Cache Issues

If you’re experiencing strange behavior, try clearing pnpm’s cache:

pnpm store prune