Turborepo Monorepo: Setup to Production
Monorepos are not a trend — they're a pragmatic solution to a real problem: code sharing across multiple applications without the overhead of versioning and publishing internal packages. Turborepo 2.8 is the tool that makes TypeScript monorepos feel fast, and when combined with pnpm 10.28's workspace protocol, you get a development experience that scales from 5 developers to 50.
This guide covers the complete journey: from workspace configuration and build pipeline design through shared package patterns and CI caching strategies. We'll draw from a production monorepo with 5 apps, 7 shared packages, and build times under 3 minutes on CI.
Key Takeaways
- Workspace packages must be pre-built to CommonJS via
tscbefore NestJS can consume themturbo.jsonpipeline tasks can depend on upstream package builds with^build- Never import from workspace packages using relative paths — use workspace protocol aliases
- Remote caching dramatically cuts CI time — configure it from day one
pnpmworkspaces are faster and more predictable thannpmoryarnworkspaces for monorepos- Filter with
--filterto build/test only affected packages during development- TypeScript
pathsmust be configured in bothtsconfig.jsonand the bundler for IDE support- Turbo's persistent tasks (
persistent: true) prevent dev servers from exiting
Repository Structure
A well-organized Turborepo separates applications from shared packages clearly. Applications are deployable; packages are libraries.
monorepo-root/
apps/
web/ — Next.js 16 frontend
api/ — NestJS 11 backend
docs/ — Docusaurus
packages/
db/ — Drizzle ORM + schema
types/ — Shared TypeScript types
validators/ — Zod schemas
utils/ — Shared utility functions
ui/ — Shared React components
email-templates/ — React Email components
config/ — Shared configuration (eslint, tsconfig)
infrastructure/
docker-compose.dev.yml
nginx/
turbo.json
package.json — Root workspace config
pnpm-workspace.yaml
The infrastructure/ directory is not a package — it contains deployment configuration and is excluded from Turbo builds.
Root Configuration
The root package.json configures the workspace and defines scripts that Turbo orchestrates:
{
"name": "ecosire-monorepo",
"private": true,
"packageManager": "[email protected]",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"test": "turbo run test",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"db:push": "pnpm --filter @ecosire/db db:push",
"db:studio": "pnpm --filter @ecosire/db db:studio",
"db:migrate": "pnpm --filter @ecosire/db db:migrate"
},
"devDependencies": {
"turbo": "^2.8.0",
"typescript": "^5.4.0"
}
}
The pnpm-workspace.yaml tells pnpm where to find packages:
packages:
- "apps/*"
- "packages/*"
turbo.json: The Build Pipeline
The turbo.json file is where Turborepo's power lives. It defines task dependencies, caching behavior, and which outputs to persist.
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**", "build/**"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"type-check": {
"dependsOn": ["^build"]
},
"db:push": {
"cache": false
},
"db:studio": {
"cache": false,
"persistent": true
}
}
}
Key concepts:
"dependsOn": ["^build"]— The caret (^) means "build all packages this task depends on first". When you rundevon the web app, Turbo automatically builds@ecosire/db,@ecosire/types, and all other dependencies first."persistent": true— Prevents Turbo from treating long-running dev servers as failures. Without this,devtasks exit immediately."cache": false— For tasks where output changes aren't meaningful (database operations, dev servers).outputs— Which files Turbo should cache. Be specific — caching.next/cache/**(Next.js build cache) wastes space.
Shared Package Setup
Shared packages are the core value proposition of a monorepo. Each package needs a package.json with build scripts and a TypeScript config.
{
"name": "@ecosire/types",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"typescript": "workspace:*"
}
}
{
"extends": "../../packages/config/tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Workspace Protocol for Internal Dependencies
When apps depend on internal packages, use the workspace:* protocol — not ^0.0.0 or a version number:
{
"name": "@ecosire/api",
"dependencies": {
"@ecosire/db": "workspace:*",
"@ecosire/types": "workspace:*",
"@ecosire/validators": "workspace:*",
"@ecosire/utils": "workspace:*"
}
}
The workspace:* protocol tells pnpm to link the local package directly instead of fetching from npm. This means changes to @ecosire/types are immediately available in apps/api after rebuilding (or instantly if you use TypeScript paths pointing to source).
TypeScript Configuration Inheritance
The packages/config package contains shared TypeScript configuration:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "CommonJS",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
{
"extends": "../../packages/config/tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"jsx": "preserve",
"paths": {
"@/*": ["./src/*"],
"@ecosire/db": ["../../packages/db/src"],
"@ecosire/types": ["../../packages/types/src"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "next.config.ts"],
"exclude": ["node_modules"]
}
The paths configuration with direct src references means TypeScript IDE support (go-to-definition, auto-import) works without rebuilding packages during development.
NestJS CommonJS Requirement
NestJS 11 requires CommonJS modules. This creates a build order constraint: workspace packages must be compiled to CommonJS before NestJS can consume them. This is why the dev task in turbo.json has "dependsOn": ["^build"] — Turbo ensures all package dependencies are built first.
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node"
}
}
Next.js uses ESModules and handles both. NestJS needs CommonJS. If a shared package outputs ESModules only, NestJS will fail with SyntaxError: Cannot use import statement in a module.
Solution: compile packages to CommonJS (the safe default), or use dual output with both "import" and "require" entries in the exports field.
Remote Caching with Turborepo
Turbo's local cache only helps on the same machine. For CI/CD, enable remote caching so every CI runner benefits from previous builds:
# Authenticate with Vercel (free for remote cache)
npx turbo login
npx turbo link
For self-hosted or enterprise setups, configure a custom remote cache:
{
"remoteCache": {
"enabled": true,
"preflight": true
}
}
Set these environment variables in CI:
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: your-team-name
TURBO_REMOTE_ONLY: false
With remote caching, a CI run that only changes the frontend app skips rebuilding the backend, database package, and all unchanged apps. In a 5-app monorepo, this can cut CI time from 8 minutes to under 2.
Development Workflow
Running all apps simultaneously for local development:
# Start everything (packages build first, then apps start in parallel)
pnpm dev
# Start specific app and its dependencies only
pnpm --filter @ecosire/web dev
pnpm --filter @ecosire/api dev
# Build specific package
pnpm --filter @ecosire/db build
# Run tests only for changed packages (compared to main branch)
pnpm turbo run test --filter="...[HEAD^1]"
The --filter flag is Turbo's killer feature for large monorepos. It understands the dependency graph — --filter @ecosire/web builds the web app AND all packages it depends on.
Common Pitfalls and Solutions
Pitfall 1: Stale build artifacts causing type errors
When you change a shared package but don't rebuild it, consuming apps see stale types. Fix:
# Clean all build outputs and rebuild
find . -name "dist" -not -path "*/node_modules/*" -exec rm -rf {} + 2>/dev/null
find . -name "*.tsbuildinfo" -not -path "*/node_modules/*" -delete 2>/dev/null
pnpm turbo run build
Pitfall 2: Circular dependencies between packages
Packages A and B cannot depend on each other. Circular dependencies in workspaces cause resolution errors and make caching unreliable. Extract the shared code to a third package C that both A and B import.
Pitfall 3: Node_modules hoisting conflicts
pnpm uses a stricter hoisting model than npm/yarn. If a package works locally but fails in CI, check if it's accidentally relying on a hoisted dependency from another package. Add explicit dependencies to the package.json that needs them.
Pitfall 4: Missing --frozen-lockfile in CI
Always use pnpm install --frozen-lockfile in CI. Without it, pnpm might update the lockfile and install different versions than your lockfile specifies, breaking reproducibility.
Frequently Asked Questions
Should I use Turborepo or Nx for a new monorepo?
Both are excellent choices. Turborepo is simpler to set up, has zero configuration overhead, and integrates naturally with pnpm workspaces. Nx has more advanced features: project graph visualization, code generation, and fine-grained affected detection at the file level. For most teams, Turborepo is the right starting point. Migrate to Nx if you need its advanced features.
How do I handle environment variables in a monorepo?
Keep a single .env.local at the monorepo root. Each app reads it using a path relative to its own __dirname. For NestJS: path.join(__dirname, '..', '..', '..', '.env.local'). For Next.js: Next.js automatically picks up .env.local files, searching up to the monorepo root. Never commit .env.local — add it to .gitignore.
How do I version and publish packages from a monorepo?
For internal packages ("private": true), no versioning is needed — use workspace:* protocol. For publishable packages, use Changesets (@changesets/cli) for version management and changelog generation. Changesets integrates with GitHub Actions for automated npm publishing on merge to main.
How does Turborepo handle task parallelism?
Turbo runs tasks in parallel by default when they have no dependencies on each other. It respects the dependency graph in turbo.json — tasks with "dependsOn": ["^build"] run after their upstream builds complete. You can control concurrency with --concurrency flag, but the default (number of CPU cores) is usually optimal.
What's the recommended way to share ESLint and Prettier configs?
Create a packages/config package with your base ESLint and Prettier configurations. Export them as plain objects or use the extends pattern. Each app/package imports the shared config using the workspace:* protocol. This ensures consistent code style across the entire monorepo without duplicating configuration.
Next Steps
Building and maintaining a production monorepo requires careful upfront architecture decisions that compound over time. ECOSIRE runs a 5-app, 7-package Turborepo monorepo in production, with sub-3-minute CI builds and zero configuration drift between apps.
If you're scaling a development team or migrating from a multi-repo setup, our engineering team can help you architect a monorepo that scales. Explore our development services to get started.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Grow Your Business with ECOSIRE
Enterprise solutions across ERP, eCommerce, AI, analytics, and automation.
Related Articles
GitHub Actions CI/CD for Monorepo Projects
Complete GitHub Actions CI/CD guide for Turborepo monorepos: affected-only builds, parallel jobs, caching strategies, environment-based deploys, and security best practices.
Power BI Deployment Pipelines: Dev to Production Workflow
Implement Power BI deployment pipelines for governed development — promote datasets and reports through Development, Test, and Production stages with automated validation and rollback.
API Gateway Patterns and Best Practices for Modern Applications
Implement API gateway patterns including rate limiting, authentication, request routing, circuit breakers, and API versioning for scalable web architectures.