Monorepo Tooling: Turborepo vs Nx, Workspace Setup, Shared Packages, and CI Optimization
Set up a production monorepo in 2026 — Turborepo vs Nx comparison, pnpm workspaces configuration, shared TypeScript packages, internal design system, CI pipelin
Monorepo Tooling: Turborepo vs Nx, Workspace Setup, Shared Packages, and CI Optimization
A monorepo keeps all related code in one repository. Instead of a separate repo for your web app, API, shared utilities, and design system, you have one repo with multiple packages. Dependencies between packages are local — no publishing to npm to use your own code.
The main challenge is build performance: as the repo grows, CI builds become slow if you rebuild everything on every change. The tools covered here (Turborepo, Nx) solve this with caching and dependency-aware task orchestration.
Turborepo vs Nx
| Turborepo | Nx | |
|---|---|---|
| Learning curve | Low — minimal config | Medium — more concepts (executors, generators) |
| Task caching | File-based, remote cache | File-based, remote cache, cloud cache |
| Code generation | None built-in | Generators for apps/libs/components |
| Framework support | Any | Strong Next.js, React, Angular, NestJS integration |
| Affected analysis | Task-graph based | Deep dependency analysis with project graph |
| Migration | Easy to add to existing repo | Requires more structure |
| Best for | Simpler repos, existing pnpm workspaces | Large repos, multiple frameworks, strict structure |
Recommendation: Start with Turborepo. If you grow into needing code generators, affected tests at scale, or framework-specific plugins, migrate to Nx.
Monorepo Structure
your-app/
├── apps/
│ ├── web/ # Next.js frontend
│ ├── api/ # Fastify backend
│ └── docs/ # Docusaurus documentation site
├── packages/
│ ├── ui/ # Shared React component library
│ ├── config/ # Shared ESLint, TypeScript, Prettier configs
│ ├── database/ # Prisma schema + generated client
│ └── utils/ # Shared utility functions
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
└── tsconfig.json # Root TypeScript config
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
pnpm Workspace Setup
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
// package.json (root)
{
"name": "your-app",
"private": true,
"engines": { "node": ">=22", "pnpm": ">=9" },
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"test": "turbo test",
"lint": "turbo lint",
"type-check": "turbo type-check",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.3.0",
"typescript": "^5.4.0"
}
}
// packages/ui/package.json — shared component library
{
"name": "@acme/ui",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"type-check": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
},
"devDependencies": {
"@acme/config": "workspace:*",
"tsup": "^8.0.0"
}
}
// apps/web/package.json — Next.js app
{
"name": "@acme/web",
"version": "0.0.0",
"private": true,
"dependencies": {
"@acme/ui": "workspace:*", // Local package — no npm publish
"@acme/database": "workspace:*",
"@acme/utils": "workspace:*",
"next": "^15.0.0"
}
}
Turborepo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"],
"pipeline": {
"build": {
"dependsOn": ["^build"], // ^ = run deps' build first
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**" // Don't cache Next.js build cache
],
"cache": true
},
"dev": {
"dependsOn": ["^build"], // Build deps before starting dev servers
"persistent": true, // Long-running task (dev server)
"cache": false
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"outputs": [],
"cache": true
},
"type-check": {
"dependsOn": ["^build"],
"outputs": [],
"cache": true
},
"clean": {
"cache": false
}
},
"remoteCache": {
"signature": true
}
}
The ^build convention: runs build on all dependencies before running build on the current package. If web depends on ui, Turborepo builds ui first automatically.
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Remote Caching
Remote cache means CI downloads cached outputs from a previous run instead of rebuilding. If packages/ui hasn't changed since the last CI run, its build output is downloaded from cache — 0ms build time for that package.
# Turborepo Remote Cache via Vercel (free)
npx turbo login
npx turbo link # Links to your Vercel account's cache
# Self-hosted remote cache with ducktape
# (Alternative to Vercel's cache — for private/on-prem)
# See: https://github.com/ducktors/turborepo-remote-cache
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # Vercel remote cache token
TURBO_TEAM: ${{ secrets.TURBO_TEAM }} # Vercel team slug
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build (cached by Turborepo)
run: pnpm turbo build
- name: Test (cached by Turborepo)
run: pnpm turbo test
- name: Lint + Type check (cached)
run: pnpm turbo lint type-check
CI time impact: Without remote cache, a full CI run might take 8 minutes. With remote cache on a PR that only touches apps/web, Turborepo skips the packages/ui and packages/database builds — total time drops to 2–3 minutes.
Shared Packages
Shared TypeScript config:
// packages/config/tsconfig.base.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
Shared database package with Prisma:
// packages/database/src/index.ts
import { PrismaClient } from '@prisma/client';
// Singleton pattern — avoid multiple PrismaClient instances in dev (Next.js hot reload)
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export * from '@prisma/client'; // Re-export all Prisma types
// packages/database/package.json
{
"name": "@acme/database",
"scripts": {
"db:generate": "prisma generate",
"db:migrate": "prisma migrate deploy",
"db:studio": "prisma studio"
},
"devDependencies": { "prisma": "^5.0.0" },
"dependencies": { "@prisma/client": "^5.0.0" }
}
Running Commands Across the Monorepo
# Run a command in a specific package
pnpm --filter @acme/web dev
pnpm --filter @acme/api build
# Run in all packages matching a pattern
pnpm --filter "./packages/**" build
# Run via Turborepo (with caching and dependency ordering)
turbo build --filter=@acme/web # Build web and its deps
turbo test --filter=@acme/ui... # Test ui and everything that depends on ui
turbo lint --filter=[HEAD^1] # Lint only packages changed since last commit
# Generate a new component in the ui package
cd packages/ui && pnpm plop component # If using plop for codegen
Working With Viprasol
We set up monorepo infrastructure for engineering teams — Turborepo or Nx configuration, pnpm workspace design, shared packages, remote caching pipelines, and CI optimization that keeps build times under 5 minutes as repos grow.
→ Talk to our team about developer tooling and monorepo architecture.
See Also
- CI/CD Pipeline Setup — GitHub Actions beyond monorepos
- Developer Onboarding — monorepo setup in the onboarding script
- TypeScript Patterns — shared types across monorepo packages
- Software Documentation — documenting monorepo architecture
- Web Development Services — full-stack architecture and tooling
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.