A monorepo (single repository for multiple packages and apps) improves code sharing, refactoring, and dependency management. In 2026, the three dominant approaches are Turborepo (fast caching, simple config), Nx (powerful generators and affected detection), and pnpm Workspaces (lightweight, no additional tooling). This guide covers setup, task caching, and CI integration for all three.
Turborepo: Setup and Structure
Turborepo is optimized for JavaScript/TypeScript monorepos. It uses a simple pipeline definition and aggressive caching to skip tasks whose inputs haven't changed.
# Create a new Turborepo monorepo
npx create-turbo@latest my-monorepo
cd my-monorepo
# Or add Turborepo to an existing monorepo
npm install turbo --save-dev
# Directory structure
my-monorepo/
├── apps/
│ ├── web/ # Next.js app
│ └── docs/ # Docs site
├── packages/
│ ├── ui/ # Shared React components
│ ├── utils/ # Shared utilities
│ └── tsconfig/ # Shared TypeScript configs
├── turbo.json # Pipeline configuration
└── package.json # Root workspace configTurborepo Pipeline Configuration
The turbo.json pipeline defines how tasks relate to each other and what outputs to cache. The ^ prefix means "wait for dependent packages to build first."
// turbo.json — pipeline definition
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"], // ^ means: run in dependency order
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false, // Never cache dev servers
"persistent": true // Long-running task
},
"type-check": {
"dependsOn": ["^build"],
"outputs": []
}
},
"globalEnv": ["NODE_ENV", "DATABASE_URL"]
}
# Run all build tasks (uses cache if inputs unchanged)
npx turbo build
# Run only for specific apps/packages
npx turbo build --filter=web
npx turbo build --filter=...ui # ui and all its dependents
# Force re-run (bypass cache)
npx turbo build --force
# View task graph
npx turbo build --graphNx: Setup and Project Graph
Nx provides a richer feature set: code generators, affected commands (only run tasks for changed code), and first-class support for many frameworks including React, Angular, Node, and .NET.
# Create Nx monorepo
npx create-nx-workspace@latest my-nx-repo --preset=ts
# Add Nx to existing monorepo
npx nx@latest init
# Generate a new app or library
nx generate @nx/next:app web
nx generate @nx/react:library ui
nx generate @nx/node:library utils
# nx.json — workspace configuration
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint", "e2e"],
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)"],
"sharedGlobals": []
}
}pnpm Workspaces: Lightweight Approach
pnpm workspaces provide native monorepo support without additional tooling. Combine with Turborepo or Nx for caching, or use alone for simpler projects.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
# Install all workspace dependencies
pnpm install
# Add a package to a specific workspace
pnpm add react --filter @myapp/web
# Add a local workspace package as dependency
# In apps/web/package.json:
{
"dependencies": {
"@myapp/ui": "workspace:*"
}
}
# Run scripts across all packages
pnpm --filter '*' run build
pnpm --filter './apps/*' run dev
pnpm --filter '...@myapp/ui' run test # ui and dependents
# Publish all public packages
pnpm publish -r --access publicRemote Caching
Remote caching shares build artifacts between developers and CI systems. A cache hit means instant builds — no recompilation needed if inputs are unchanged.
# Turborepo Remote Cache (Vercel)
# Enables sharing build cache across developers and CI
# 1. Link to Vercel
npx turbo login
npx turbo link
# 2. Now CI builds share cache with local dev:
# CI run: builds from scratch, uploads to cache
# Developer: pulls cache, gets instant builds
# Self-host with Turborepo Remote Cache (open source)
# docker run -p 3000:3000 ducktors/turborepo-remote-cache
# .turbo/config.json (auto-generated by turbo link)
{
"teamId": "team_xxx",
"apiUrl": "https://vercel.com"
}
# Nx Cloud (similar offering from Nx)
# nx connect-to-nx-cloud
# Provides remote caching + task distributionVersioning with Changesets
Changesets is the standard tool for versioning and publishing packages in a monorepo. It tracks which packages changed and generates CHANGELOG.md entries.
# Changesets — versioning and publishing for monorepos
# Works with npm/yarn/pnpm workspaces
pnpm add -D @changesets/cli
pnpm changeset init
# When you make a change that needs a version bump:
pnpm changeset
# → Interactive prompt: which packages changed, bump type (major/minor/patch)
# Creates a markdown file in .changeset/ describing the change
# Example .changeset/funny-bears-dance.md:
---
"@myapp/ui": minor
"@myapp/utils": patch
---
Add new Button component with loading state
# Apply changesets (bumps versions, updates CHANGELOG.md)
pnpm changeset version
# Publish all changed packages
pnpm changeset publishTurborepo vs Nx vs pnpm Workspaces
| Feature | Turborepo | Nx | pnpm Workspaces |
|---|---|---|---|
| Setup complexity | Low | Medium | Very Low |
| Task caching | Built-in | Built-in | Manual/external |
| Remote cache | Vercel (free tier) | Nx Cloud (paid) | None built-in |
| Code generators | No | Yes (rich) | No |
| Affected detection | Basic (--filter) | Advanced (nx affected) | Via Changesets |
| Language support | JS/TS focused | Polyglot | Any |
| Learning curve | Low | Medium-High | Low |
Best Practices
- Start with pnpm workspaces + Turborepo — lowest friction, excellent caching, good documentation.
- Define clear boundaries: shared packages in packages/, applications in apps/. Never import app code from a shared package.
- Use Changesets for versioning and publishing. Automate releases with GitHub Actions on merge to main.
- Enable remote caching early — Turborepo's Vercel cache is free for hobby projects. CI time drops from minutes to seconds.
- Keep each package's package.json explicit: list all direct dependencies even if they are hoisted. This prevents phantom dependency bugs.
Frequently Asked Questions
Should I use Turborepo or Nx?
Turborepo is simpler and faster to set up — ideal for most JavaScript/TypeScript projects. Nx has more features: code generators, affected detection, module boundaries enforcement, and better support for polyglot repos (Java, .NET). Choose Nx if you need those features or are working on a large enterprise repo.
What is the difference between monorepo and polyrepo?
A monorepo stores all code in one repository. A polyrepo (multi-repo) has separate repositories per package or service. Monorepos make cross-package refactoring easier and ensure all packages are always compatible. Polyrepos have simpler CI and more team autonomy. Most companies at scale use monorepos.
How does Turborepo caching work?
Turborepo hashes the inputs to each task (source files, environment variables, dependencies). If the hash matches a previous run, it restores outputs from cache (locally or remotely). The cache is content-addressable, so identical inputs always produce cache hits regardless of when they were created.
Can I have different package managers in a monorepo?
No. All workspaces in a monorepo must use the same package manager. The workspace protocol (workspace:*) is implemented differently in npm, yarn, and pnpm. Pick one and stick with it. pnpm is the most efficient for monorepos due to its symlink-based node_modules.
How do I handle environment variables in a monorepo?
Define environment variables per app (each app has its own .env). For shared secrets, use a secrets manager (Vault, AWS SSM). In Turborepo, list environment variables in turbo.json globalEnv (for all tasks) or per-task env array. Turborepo includes env vars in the cache key to prevent stale cache hits.