Every team building multi-package products asks the same question: monorepo or polyrepo? And if monorepo, which tool? Nx promises the world (build graphs, affected-only runs, code generation). Turborepo promises speed (incremental builds, caching, minimal config). pnpm workspaces? It's just the package manager doing what it should — hoisting shared dependencies, phantom-linking packages, no node_modules bloat. The internet consensus is wrong: you do not need Nx. You probably don't even need Turborepo alone. Aidxn's stack for multi-package projects (Velocity X templates, multi-seat billing platforms, design-system + app combos) is pnpm workspaces + Turborepo's task runner, and it covers 90% of real-world monorepo needs. Nx graduates onto that stack when you hit 10+ packages and need build graphs that know which packages changed. Until then, pnpm + Turborepo is lighter, faster to scaffold, and zero magic.
Three Tools: Scope, Complexity, Sweet Spot
pnpm Workspaces
What it does: Manages package dependencies at the monorepo level. Instead of each package having its own `node_modules`, pnpm creates a single shared store and symlinks packages together. Saves disk space (no duplication), speeds up installs, and enforces a single version of shared dependencies across the repo. No build orchestration, no task caching, no code generation. It's a package manager, not a build system. When to use: You have 2–5 related packages (e.g., `@myapp/ui` + `@myapp/api` + `@myapp/web`), they share dependencies, you want fast installs and phantom-linking. Setup complexity: 5 minutes. Add a `pnpm-workspace.yaml` at the repo root listing package directories, set up `package.json` with `"workspaces": ["packages/*"]`, and you're done. Overhead: Nearly zero. You're not adopting a new framework; you're letting pnpm do its job.
Turborepo
What it does: Sits on top of pnpm (or npm/yarn) and adds task orchestration + caching. You define tasks in `turbo.json`, and Turborepo handles dependency graphs between tasks, caches outputs, and can run tasks in parallel or sequence. Task cache hits mean zero work — outputs are restored from disk instantly. No build graph, no code generation. It's a task runner, not a build system. When to use: You have 3–10 packages with inter-package dependencies (e.g., `web` depends on `ui` depends on `shared`), you're tired of running build commands in the right order, and you want caching to speed up CI. Setup complexity: 15 minutes. Define tasks, add cache keys, run `turbo run
Nx
What it does: Builds a complete build graph of every package and task in your repo. Uses that graph to know which packages changed, and runs only the tasks that are affected by those changes. Adds code generation, testing scaffolds, linting rules, and workspace visualization. Full monorepo platform. When to use: You have 10+ packages, you're tired of slow CI builds, you need to onboard developers to a consistent workspace structure, and you want code generation and workspace schematics. Setup complexity: 30+ minutes. Nx has opinions about folder structure, generator templates, and plugin systems. You're learning a lot of new concepts. Overhead: High. You're adopting a framework. There's config, plugins, and build graph maintenance.
Side-by-Side: Install Speed, Caching, Task Orchestration, Learning Curve
pnpm Workspaces
Install speed: Fastest. pnpm deduplicates aggressively and reuses cached packages from previous installs. A 5-package monorepo with shared dependencies installs in 12–20 seconds (cold) on modern hardware. Caching: None. Each task run rebuilds. If `api` changes and you rebuild `web`, both packages rebuild even if they didn't change. Task orchestration: Manual. You write shell scripts or npm scripts that run tasks in dependency order. `npm run build` at the root needs to know `build:api` then `build:ui` then `build:web`. Learning curve: Flat. If you know `npm`/`yarn`, you know pnpm. It's the same mental model, slightly better disk usage.
Turborepo
Install speed: Same as pnpm (they're the same package install). Turborepo doesn't change installation, only task running. Caching: Smart. Turborepo caches task outputs (e.g., the dist folder from a build). If you run `turbo run build` twice without changing code, the second run hits cache and restores outputs instantly. If a package's dependencies didn't change (same input hash), cache hits. Cold cache: ~30 seconds for a 5-package build; warm cache: ~2 seconds. Task orchestration: Automatic. Define task dependencies in `turbo.json` (`"build": { "dependsOn": ["^build"] }`), and Topological sort happens automatically. Parallelization is free. Learning curve: Low. You learn one JSON config format (cache keys, task dependencies). Most projects can get 95% benefit from a boilerplate `turbo.json` without understanding every option.
Nx
Install speed: Same as pnpm. Nx doesn't manage installation. Caching: Intelligent and granular. Nx watches file hashes at a fine-grained level and knows when any task input changed. More sophisticated than Turborepo's approach, but also more overhead. Task orchestration: Automatic and affected-aware. `nx affected:build` runs only build tasks for packages that changed since the last commit. Saves enormous time in CI if your build suite is slow. Plugin system & code generation: Nx can scaffold packages, generate components, migrate dependencies. Tooling ecosystem is mature. Learning curve: Steep. Nx has plugins, executors, generators, workspace.json/nx.json configs, and a whole mental model (project graph, task graph). Expect 2–3 days to get a large team productive.
Real-World Setup: pnpm Workspaces + Turborepo
Folder Structure
Start with this layout for a 5-package project (shared design system, API, web app, mobile app, docs):
monorepo-root/
├── pnpm-workspace.yaml
├── package.json
├── turbo.json
├── packages/
│ ├── ui/
│ │ ├── package.json
│ │ ├── src/
│ │ └── dist/
│ ├── api/
│ │ ├── package.json
│ │ ├── src/
│ │ └── dist/
│ ├── web/
│ │ ├── package.json
│ │ ├── src/
│ │ └── .next/
│ ├── mobile/
│ │ ├── package.json
│ │ └── src/
│ └── docs/
│ ├── package.json
│ └── src/
└── node_modules/ (managed by pnpm, shared)
Step 1: pnpm Workspaces Config (5 minutes)
Root `pnpm-workspace.yaml`:
packages:
- 'packages/*'
Root `package.json`:
{
"name": "monorepo",
"version": "0.0.1",
"private": true,
"engines": { "pnpm": ">=8.0.0" },
"devDependencies": {
"turbo": "^1.11.0"
}
}
Each package gets its own `package.json` with interdependencies as imports (not file paths). Example, `web/package.json`:
{
"name": "@monorepo/web",
"version": "0.0.1",
"dependencies": {
"@monorepo/ui": "workspace:*",
"react": "^19.0.0"
}
}
The `workspace:*` syntax tells pnpm to resolve `@monorepo/ui` from the local packages, not npm. Run `pnpm install` at the root and pnpm phantom-links all packages together.
Step 2: Turborepo Task Config (10 minutes)
Root `turbo.json`:
{
"$schema": "https://turbo.build/json-schema.json",
"globalDependencies": ["**/.env"],
"pipeline": {
"build": {
"outputs": ["dist/**", ".next/**"],
"cache": true,
"dependsOn": ["^build"]
},
"test": {
"outputs": ["coverage/**"],
"cache": true,
"dependsOn": ["^build"]
},
"lint": {
"outputs": [""],
"cache": false
},
"type-check": {
"cache": false
}
}
}
Breakdown: `"build"` depends on `^build` (caret means "dependencies must build first"). Outputs (like `dist/`) are cached. On a second run without code changes, Turborepo restores from cache. `"lint"` and `"type-check"` don't cache (they're fast enough and you want immediate feedback). `"globalDependencies"` means if `.env` changes, clear all caches (safety net for env-sensitive builds).
Step 3: Root npm Scripts
Root `package.json` now adds:
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"dev": "turbo run dev --parallel",
"type-check": "turbo run type-check"
}
}
Run `pnpm run build` and Turborepo handles dependency order, caching, and parallelization automatically. The second time you run it (with no changes), you'll see cache hits: `web/web:build cache hit, skipping, ...` or similar. Task runs complete in milliseconds.
Step 4: Adding a New Package
Create `packages/newpkg/package.json` with a unique scoped name (`@monorepo/newpkg`), add its build script, and you're done. pnpm automatically picks it up. Turborepo sees the new entry the next time you run a task. No root config changes needed.
When to Graduate to Nx: The 10-Package Threshold
Stay with pnpm + Turborepo if: 2–8 packages, <50 developers, build times <5 minutes, no complex code-generation needs. The friction is low and ROI is high.
Graduate to Nx if: 10+ packages, large team (50+), build times >10 minutes, frequent scaffolding, need affected-only CI (only run tests for changed packages), or explicit onboarding-heavy workspace standards. The Nx build graph becomes worth the config overhead.
Example graduation story: A team started with 4 packages (pnpm + Turborepo, 8-minute build). Over a year, they grew to 12 packages. Turborepo's cold cache hit ~40 seconds; CI was running full builds even for single-file changes in one package. They migrated to Nx, added `affected` logic to CI, and cut CI time from 8 minutes to 2 minutes (only affected packages build/test). Nx's `nx dep-graph` also helped onboard junior developers who previously didn't understand package dependencies. Migration took a week; payoff justified.
Six FAQs
Can I use npm or yarn instead of pnpm?
Yes. pnpm workspaces are lighter than npm/yarn workspaces (pnpm's phantom linking is faster), but npm workspaces and yarn workspaces work. Turborepo works with all three. If your team is already deep in npm, stick with it. The performance difference (seconds, not minutes) isn't worth the migration friction.
Do I need a monorepo at all? Can I use a polyrepo instead?
If you have truly independent packages (e.g., separate products, separate teams, separate release cycles), polyrepo is simpler. But if you have a shared UI library used by 3 apps, or a shared API client, or shared utilities, a monorepo saves friction: single linting config, single TypeScript version, single test runner, atomic commits across packages. Polyrepo means version-bumping the shared lib, rebuilding, republishing to npm, then updating all consumers. Monorepo means one commit, one build. For Aidxn's Velocity templates and multi-tenant platforms, monorepo always wins.
Does Turborepo work with Docker multi-stage builds?
Yes. Turborepo caches are filesystem-based (local disk). In a Docker multi-stage build, you can copy the `.turbo/cache` folder between stages (or use Docker layer caching). For CI, you can also use Turborepo's remote caching (via Vercel's Turborepo Remote Caching service) to share cache between CI runs and local machines.
What if I want to migrate from Turborepo to Nx later? Will I be locked in?
No. Your package structure (folder layout, package.json names, build scripts) is the same. Migrating to Nx means adding workspace.json / nx.json and learning Nx's executor model, but your source code and inter-package dependencies are unchanged. It's a tool swap, not an architecture change.
How do I handle secrets and environment variables in a monorepo?
Use `.env.local` at the root (shared secrets), and `.env.local` in each package directory (package-specific secrets). Turborepo's `globalDependencies` can watch root `.env.local` to invalidate cache if secrets change. For CI, use your CI provider's secret management (GitHub Secrets, etc.) and inject via environment variables in the build step. Never commit secrets to git.
Can I deploy individual packages from a monorepo to separate hosts?
Yes. Each package is independent (it has its own build output, its own package.json, its own deploy script). You can deploy `packages/api` to a backend host and `packages/web` to a static host in the same CI pipeline. Turborepo's task caching means you only rebuild what changed. If only `web` changed, API skips build entirely.
The Bottom Line
pnpm workspaces + Turborepo is a boring, un-sexy, boring monorepo setup that works for 90% of real projects. No magic, no code generation, no build graphs, no plugin ecosystem. Just faster installs, smart task caching, and dependency ordering that you define once in JSON. Aidxn uses it for Velocity X (multi-package design system + Next.js apps), and it has never let us down. Nx is great if you have 10+ packages and a large team — the affected-aware builds and workspace tooling pay for the complexity. But if you're starting a monorepo, start here: pnpm + Turborepo. You'll feel how much faster it is than polyrepo chaos, and you'll keep it simple. Graduate to Nx when you actually need it, not before.
Building a multi-package product? Start with our architecture consultation to structure your monorepo for scale, or check out our deep dive on testing across monorepos to keep quality high as you grow.