What a monorepo needs (the real requirements)
Source: Dev.to
What a monorepo package manager should handle
- Workspaces (multiple packages/apps)
- Fast installs for large repositories
- Deterministic lockfile (same dependencies everywhere)
- Good CI caching
- Minimal “dependency weirdness” (phantom deps, hoisting surprises)
- Smooth developer workflow (run scripts across packages, filtering, etc.)
pnpm advantages
Global content‑addressable store
- pnpm keeps a single copy of each package version in a global store.
- Each project uses hard‑links into
node_modules. - Result: repeated installs are very fast, and huge monorepos benefit a lot.
Smaller repo footprint
- With npm, each workspace can duplicate the same packages across subprojects.
- With pnpm, packages are reused from the store.
- Result: smaller repository size and faster Docker/CI layers.
Strict dependency handling
- A package cannot import a dependency that it does not declare in its own
package.json. - This prevents the classic monorepo bug: “It worked on my machine because it was hoisted… but CI broke later.”
Ergonomic workspace commands
- Run scripts across all packages.
- Filter to run only on affected packages.
- Clean recursive installs/builds.
Effective caching
- Because pnpm’s store is reusable, caching is simpler and more effective.
- In many CI pipelines, pnpm installs become consistently faster after the first cached run.
npm considerations
- npm ships with Node by default.
- Some older tools and scripts assume npm‑style hoisting behavior, making npm “boring and safe” in certain enterprise setups.
- If the team already knows npm and the repository is small‑ish, npm workspaces can be “good enough.”
- npm often hoists dependencies, allowing code to import packages that aren’t declared, which can hide missing‑dependency problems until later.
Choosing between pnpm and npm
| Aspect | npm | pnpm |
|---|---|---|
| Dependency handling | Hoisted dependencies may appear available even if not declared | Symlinked structure enforces correctness; missing dependencies are caught early |
| Repository size | Larger due to duplicated packages | Smaller thanks to shared store |
| Install speed | Slower for large repos, especially on repeated installs | Faster, especially after first cached run |
| Suitability | Small repos, environments that rely on hoisting | Bigger repos, repeated installs, teams that value strictness |
Pick pnpm if you want:
- Faster, deterministic installs for large monorepos
- Strict dependency enforcement
- Better CI caching
Pick npm if you want:
- Compatibility with legacy tools that expect hoisting
- Simplicity for small repositories
Recommended setup (pnpm)
- Keep a single
pnpm-lock.yamlat the repository root. - Use pnpm workspaces.
- Pin the pnpm version for consistency across the team, e.g.:
{
"packageManager": "pnpm@9.1.2"
}
- Define the workspace layout in
pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
My default in 2026: pnpm for almost every new monorepo.