A monorepo keeps multiple packages or applications in one git repository. The appeal is sharing code, coordinating versioning, and keeping cross-cutting changes in a single PR. The trap is turning it into a slow, undifferentiated blob. In 2026, the tooling is good enough that the setup is straightforward — the hard part is keeping package boundaries clean.
What changed in 2026
- Turborepo 3 added native support for
--affected flag, running tasks only for packages touched since the base branch — the same feature Nx had as its main differentiator.
- pnpm 10 improved workspace protocol resolution and hoisting behavior;
pnpm-lock.yaml is now fully deterministic across platforms.
- Nx 20 introduced project crystal, inferring project graph from file structure without an
nx.json configuration file for most frameworks.
- Bun workspaces are production-stable; Bun monorepos are ~3× faster for install and script execution — worth evaluating for new TypeScript-only repos.
Directory structure
my-monorepo/
├── apps/
│ ├── web/ # Next.js app
│ └── api/ # Fastify server
├── packages/
│ ├── ui/ # Shared React components
│ ├── config/ # Shared ESLint, TS, Prettier configs
│ └── utils/ # Shared utilities
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
└── tsconfig.base.json
Step 1 — pnpm workspaces
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
Root package.json:
{
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev --parallel",
"lint": "turbo lint",
"test": "turbo test"
},
"devDependencies": {
"turbo": "^3.0.0"
}
}
Install from the root:
pnpm install
Step 2 — Turborepo pipeline
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
"^build" means: build all dependencies before building this package.
Step 3 — shared TypeScript config
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"moduleResolution": "bundler",
"paths": {}
}
}
Each package extends it:
// packages/utils/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
Remote caching
Remote caching is the highest-leverage Turborepo feature. It shares build artifacts across all developers and CI machines:
# Authenticate with Vercel Remote Cache (free)
npx turbo login
npx turbo link
Add to CI:
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
With remote caching, a CI run that builds only changed packages drops from 5 min to ~30 s on warm cache.
Tooling comparison
| Tool |
Strengths |
Weaknesses |
| Turborepo |
Simple setup, Vercel remote cache free |
Less powerful affected graph than Nx |
| Nx |
Affected commands, generators, plugins |
Steeper config, heavier |
| Lerna |
Historical — avoid in 2026 |
Unmaintained diff; superseded |
| Bun workspaces |
Fastest install + run |
Smaller ecosystem, less Node compatibility |
How to pick
- Small to mid team, mostly TypeScript? → pnpm + Turborepo. 30 minutes to set up.
- Large org, multiple languages, complex dependency graph? → Nx.
- Greenfield TypeScript-only, performance is priority? → Bun workspaces.
- Publishing packages to npm? Add Changesets for versioning:
pnpm add -D -w @changesets/cli.
Common mistakes
Putting everything in one package. A monorepo where packages/shared contains 80 % of the code is a monolith with extra steps. Keep packages small and single-purpose.
Not setting "private": true on workspace packages. Without it, pnpm publish can accidentally publish internal packages.
Circular dependencies. Package A importing from package B which imports from A breaks Turborepo's task ordering. Use the madge tool to detect cycles.
Ignoring cache invalidation. If turbo.json outputs do not match the actual build output directories, caching silently does nothing.
What to skip
- Lerna. It is effectively legacy. pnpm workspaces handles package management; Turborepo handles task running.
- Yarn workspaces for new projects. pnpm is strictly better: faster installs, correct dependency isolation, no phantom dependencies.
- Custom task runner scripts. Before writing
scripts/build-all.sh, check if turbo build --filter=... already does what you need.
FAQ
Can I mix JavaScript and TypeScript packages?
Yes. Configure each package independently. TypeScript packages compile to dist/; JS packages can export from src/ directly.
How do I add a new package?
Create the directory under apps/ or packages/, add a package.json with a unique name, and run pnpm install from the root.
Should apps and packages be in separate directories?
Convention says yes: apps/ for runnable applications (web, API, CLI), packages/ for libraries. This makes intent clear and simplifies tooling rules.
How do I run a command in only one package?
turbo build --filter=web runs only the web app and its dependencies. Or pnpm --filter=web build for a single package without Turbo.
Where to go next