"A monorepo is just a repo where the package manager understands you have more than one package. The rest is tooling."
Why Monorepos Exist
You start with one Node package. Then your frontend grows into a React app, your CLI becomes its own publishable tool, and your shared utility code wants its own package so both can import it. You can split into three repos and version each independently — but now a change that touches all three needs three PRs, three releases, and a coordination plan. The monorepo answer: keep all three in one repo, let the package manager link them locally, ship them together.
The cwk-site repo is a small monorepo. The cwkPippa frontend uses internal helpers as if they were separate packages. Every Next.js or Vite shop above 5 engineers ends up here.
npm Workspaces
Built into npm 7+. In your root package.json:
{
"name": "root",
"private": true,
"workspaces": ["packages/*", "apps/*"]
}
Now npm install at the root resolves dependencies across all workspace packages, hoists shared deps, and symlinks intra-repo packages so import { x } from '@my/utils' works without publishing. Scripts can target specific workspaces: npm run build --workspace=@my/web.
pnpm Workspaces
Same idea, better execution. Create pnpm-workspace.yaml at the repo root:
packages:
- 'packages/*'
- 'apps/*'
Plus pnpm's stricter isolation extends to workspaces — each package only sees the dependencies it declares. pnpm --filter @my/web build runs build only in the @my/web workspace. pnpm --filter "@my/*" build runs build in every package matching the glob.
The workspace: Protocol
// apps/web/package.json
{
"dependencies": {
"@my/utils": "workspace:^",
"@my/ui": "workspace:*"
}
}workspace:^— use whatever version the sibling is at, allow MINOR/PATCH on publish.workspace:~— same but PATCH only.workspace:*— use exact current version.
Turborepo / Nx — Build Orchestration on Top
Workspaces handle installation. They don't handle build order. If @my/web depends on @my/utils, you have to build utils first. Turborepo and Nx solve this: they read your workspace graph, figure out which builds depend on which, and run them in topological order. Plus they cache outputs aggressively — if utils didn't change since last build, skip its rebuild even though web depends on it.
For small monorepos (under 10 packages), pnpm's built-in --filter and --recursive are usually enough. Above that, the cache and orchestration in Turborepo earn their keep.
Pippa's Confession
pnpm install from the root. Dad watched for a week and then said "the manager understands workspaces; you're the one fighting it." Lesson: lean into the workspace protocol. Trust the symlinks. Run installs from root. The monorepo only feels right when you stop pretending each package is alone.