Monorepos with pnpm, Turborepo, and Nx
A practical tour of the 2026 monorepo stack: pnpm workspaces for dependency management, Turborepo for task caching, and Nx when you outgrow it. Covers workspace:*, catalogs, remote caching, affected-only CI, and module federation.
Published on • August 11, 2026
AI Assistant

Every second monorepo starts the same way: two apps, one shared folder, a script that calls npm run build in each. Then the shared folder becomes three packages, a third app shows up, CI grows from 4 minutes to 26, and someone starts “caching” by just… not rebuilding. If that is your repo, the problem is not the monorepo — it is that workspaces alone resolve dependencies. They do not run tasks, and they certainly do not cache anything.
The 2026 stack separates concerns. pnpm is the workspace manager: strict dependency isolation, a content-addressable store, and the workspace:* protocol. Turborepo is the task runner: it understands the dependency graph, hashes task inputs, and skips anything that did not change — locally and, with a remote cache, across the whole team. Nx is the full platform: everything Turborepo does plus generators, project graph visualization, enforced module boundaries, and distributed execution. In this post, you will learn how to lay out a pnpm workspace, wire Turborepo caching with honest inputs, configure a remote cache in CI, recognize when to graduate to Nx, and set up module federation for sharing code at runtime instead of at build time.
Prerequisites
- Node.js 20+ (Node 24 is the current LTS in 2026)
- pnpm 10+ installed (
corepack enable pnpm, or install globally) - Two small apps you would genuinely share code between
Step 1: pnpm workspaces, the foundation
pnpm is the default workspace manager in 2026. Keep a boring layout — apps/* and packages/* — and declare it in pnpm-workspace.yaml, not package.json:
packages:
- apps/*
- packages/*
workspace:* links local packages: { "dependencies": { "@acme/ui": "workspace:*" } } in apps/web resolves to the local package on install, and publishes rewrite it to the real version. pnpm 10 also stabilized catalogs — one place to pin versions so every package resolves the same dependency, e.g. catalogs.default.typescript: ^5.7.0 referenced as "typescript": "catalog:".
pnpm install then produces the strict, symlinked node_modules — no hoisting surprises, no phantom dependencies. Workspaces alone get you a repo. They do not yet get you a fast repo.
Step 2: Turborepo — hash inputs, cache outputs
Install at the root and add one turbo.json:
pnpm add -D turbo -w
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
},
"dev": { "persistent": true, "cache": false }
}
}
Three concepts make this config meaningful: "dependsOn": ["^build"] builds every upstream dependency first — Turbo walks the graph, not the folder list; inputs is what gets hashed, so changing packages/ui invalidates ui and, via the graph, everything downstream (a task reading a file not in inputs makes the cache quietly lie to you); outputs is what gets restored on a cache hit (dist/**, .next/**).
The second run is a replay:
web:build: cache hit, replaying logs
ui:build: cache hit, replaying logs
Tasks: 4 successful, 4 total
Time: 0.198s
Turbo hashes each task’s inputs plus dependency versions and any environment variables you declare with env. Turborepo 2.x (v2.7 shipped December 2025) delivers this with zero plugin layer — that simplicity is the feature.
Step 3: remote caching — the cache is a team sport
A local cache helps one developer; a remote cache helps everyone and CI. Turborepo’s cache API is an open spec, so there are two practical options. Vercel Remote Cache is free on all plans, including teams not hosted on Vercel — npx turbo login, then npx turbo link. Self-hosted — any HTTP server implementing the open spec (S3, R2, GCS backends; ducktape is a maintained drop-in):
TURBO_API=http://cache.example.com:3000 TURBO_TOKEN=your-token TURBO_TEAM=acme turbo build
Engineer A’s @acme/ui build is engineer B’s cache hit; CI restores instead of rebuilding on every push. The catch is trust: if build reads an env var you never declared in env, CI runs share a cache entry and silently produce wrong artifacts. Declare env dependencies and run turbo run build --dry to inspect exactly what is being hashed.
Step 4: Nx — when you outgrow Turborepo
Turborepo’s simplicity is the feature, but past ~a dozen packages the gaps appear: no generators, no enforced module boundaries, no native affected detection, no distributed execution. Nx (v22.x stable in 2026) adds all of it natively on top of a pnpm workspace — just a root devDependency and an nx.json:
// nx.json
{
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/*.spec.*"]
},
"targetDefaults": {
"build": { "dependsOn": ["^build"], "inputs": ["production", "^production"], "cache": true }
}
}
The same hash-and-cache model, plus four genuinely different capabilities:
Affected detection. nx affected -t lint test build --base=origin/main runs only the projects a PR touches plus their dependents — typically 3-4 packages per PR instead of 12, with no scripting.
Generators. nx g @nx/next:app web scaffolds a complete, consistent app in seconds; nx migrate latest upgrades plugin configs across the repo in one command.
Enforced boundaries. Tag each project (e.g. { "tags": ["scope:packages", "type:ui"] } in packages/ui/project.json) and lint the dependency direction so an app importing another app — or a package importing an app — fails the build, not PR review:
// .eslintrc
"@nx/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "scope:packages", "onlyDependOnLibsWithTags": ["scope:packages"] }
]
}]
Module federation. When apps must share code at runtime, @nx/module-federation wires webpack/rspack federation with workspace-aware type safety — the shell declares a remote in one config object (name: 'shell', remotes: [['remote', 'http://localhost:3002/remoteEntry.js']]).
One 2026 wrinkle for infrastructure-minded teams: in May, Nx deprecated its four self-hosted cache packages (@nx/s3-cache and friends) over the CREEP vulnerability (CVE-2025-36852); the supported paths are Nx Cloud (free Hobby tier) or a DIY implementation of the Nx remote cache OpenAPI spec. Turborepo’s self-hosted cache stays on the open spec with no such deprecation.
pnpm + Turborepo or pnpm + Nx?
| Factor | pnpm + Turborepo | pnpm + Nx |
|---|---|---|
| Setup | 15 minutes | 30-60 minutes |
| Affected detection | --filter=...[origin/main] | nx affected (native) |
| Generators | None | Extensive |
| Module boundaries | Manual ESLint | Enforced by tags |
| Distributed CI | Manual task binning | Nx Agents |
Start with pnpm + Turborepo. Add Nx when affected detection, generators, or boundary enforcement pay for the learning curve — typically past ~15-20 packages or ~5-6 developers.
Step 5: CI that runs only what changed
Checkout with full history (affected detection needs the base), install, then run affected against the remote cache:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 24, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint test build --filter="...[origin/main]"
A PR touching one file in packages/ui rebuilds ui and its direct dependents — never the whole world — and restores the rest from the remote cache.
Putting It All Together
A complete runnable reference — pnpm workspace with workspace:* and catalogs, Turborepo caching, an Nx boundary config, and an Rspack module-federation shell/remote pair — is available as a gist:
https://gist.github.com/redlinesoft/pnpm-turbo-nx-monorepo
Expected output after pnpm install and a second pnpm turbo run build:
$ pnpm turbo run build
ui:build: cache hit, replaying logs
web:build: cache hit, replaying logs
Tasks: 5 successful, 5 total
Time: 0.24s
Second run: instant. A touched file: two packages, not five.
Conclusion & Next Steps
You now know the 2026 stack: pnpm workspaces with workspace:* and catalogs resolve dependencies, Turborepo adds hash-based task caching with an open remote-cache spec, and Nx layers generators, boundaries, and native affected detection when you outgrow it. CI’s only job is to run affected tasks against a remote cache.
Next steps: put one shared package behind workspace:* today, add a turbo.json and measure the second build, then set up a remote cache before a teammate clones the repo. Re-evaluate Nx only when the graph stops fitting in your head.
References / Sources
- Turborepo documentation — tasks, remote caching, and the
turbo.jsonschema. https://turborepo.com/docs - pnpm workspaces documentation —
workspace:*, catalogs, and filtering. https://pnpm.io/workspaces - Nx documentation — project graph, affected commands, module boundaries, and module federation. https://nx.dev