Skip to content

Managing Monorepos Without the Headache Using pnpm Workspaces

Managing multiple packages in a single repository—a monorepo—is a bit like managing an ecosystem.

3 min read
pnpm
Workspaces
Monorepo
Dependencies
Node.js

Managing multiple packages in a single repository—a monorepo—is a bit like managing an ecosystem. If you aren't careful, something that started out organized can turn into a mess in an instant. The issue isn't just the folder structure; it’s how your package manager handles the dependencies between those packages.

I used to run into a classic problem: a package would work perfectly on my laptop but fail immediately in CI. It turned out some dependencies were "sneaking" into my node_modules without ever being officially declared.

Eventually, I decided to switch to pnpm workspaces. Not just because it was the trendy choice, but because its strictness actually helped me catch these issues much earlier.

Why being strict matters

In my current setup, I make sure every package is honest about what it uses. If a package needs zod or clsx, it has to be explicitly listed in its own package.json. You can't just rely on a neighboring package.

With npm or classic yarn, you can often import something you never actually declared because it was "hoisted" to the root folder. This is risky. Everything feels fine until a different package gets updated and that hidden dependency suddenly disappears. That’s usually when things break.

I also have a strict rule regarding peer dependencies. This is a common trap, especially when building your own design system or UI library. For example, if your UI package needs React, the right way is to list it as a peer dependency, not a regular dependency. If you mess this up and put it under dependencies, you might end up running two different versions of React at once. The result? That incredibly annoying "Invalid hook call" error that always seems to show up on a Friday afternoon when you're ready to head home.

Keeping versions in sync

Another headache in a monorepo is version drift. Imagine having twelve different packages where everyone manually types in the TypeScript version. Eventually, they’re going to get out of sync.

To avoid this, I prefer using the catalogs feature or at least using pnpm.overrides at the root level. That way, if a package insists on using a buggy old version, we can force it back into line from a central location.

I’ve also started adding shortcuts to my root scripts. I learned the hard way that typing out long filter commands like pnpm --filter @acme/web... build is exhausting and prone to typos. If you forget those three dots at the end, the package you intended to build gets left behind. It makes much more sense to just create a short command in the main package.json so the team doesn't have to memorize complex syntax while racing against a deadline.

A note on lockfiles

Lastly, a quick note on the lockfile. I never ask my teammates to read through thousands of lines of changes in pnpm-lock.yaml during a Pull Request. It’s torture.

However, if there’s a massive, suspicious change, I expect a brief explanation in the PR description. A simple "adding date-fns for time logic" is much more helpful than leaving 400 lines of mystery changes. If a weird package suddenly appears, I usually just run pnpm why <package-name> to find out exactly who brought it into the project.

Nothing is perfect on the first try, and every tech choice has its downsides. But with a clear structure, at least you won't be flying blind when things go wrong.

Enough for this note.