Monorepo Management with Turborepo: A Practical Guide

February 15, 2026

Monorepos have become the standard for managing multiple related projects. Turborepo, built by Vercel, provides exceptional build performance through intelligent caching and task orchestration. Here's how to set up a production-ready monorepo.

Why Turborepo?

Traditional monorepo tools can be complex and slow. Turborepo solves this with:

  • Incremental builds - Only rebuild what changed
  • Remote caching - Share build artifacts across team and CI
  • Smart task scheduling - Parallel execution with dependency awareness
  • Zero configuration - Sensible defaults that just work

Initial Setup

# Create a new Turborepo project npx create-turbo@latest my-monorepo # Or add to existing monorepo npm install turbo --save-dev

Project Structure

A typical Turborepo structure separates apps from shared packages:

my-monorepo/ ├── apps/ │ ├── web/ # Next.js web app │ ├── mobile/ # React Native app (future) │ └── docs/ # Documentation site ├── packages/ │ ├── ui/ # Shared React components │ ├── config-eslint/ # Shared ESLint config │ ├── config-typescript/ # Shared TypeScript config │ └── utils/ # Shared utility functions ├── package.json ├── turbo.json └── pnpm-workspace.yaml

Setting Up Workspace

packages: - "apps/*" - "packages/*"
{ "name": "my-monorepo", "private": true, "scripts": { "dev": "turbo dev", "build": "turbo build", "test": "turbo test", "lint": "turbo lint", "type-check": "turbo type-check" }, "devDependencies": { "turbo": "^2.0.0", "typescript": "^5.3.0" }, "packageManager": "pnpm@9.0.0" }

Configuring Turborepo

{ "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "dev": { "cache": false, "persistent": true }, "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] }, "lint": { "dependsOn": ["^lint"] }, "type-check": { "dependsOn": ["^type-check"] } } }

Key Configuration Options:

  • dependsOn: ["^build"] - Wait for dependencies to build first
  • outputs - Define what to cache
  • cache: false - Disable caching for dev servers
  • persistent: true - Keep task running (dev servers)

Creating a Shared UI Package

{ "name": "@repo/ui", "version": "0.0.0", "private": true, "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { "build": "tsup src/index.tsx --format esm,cjs --dts --external react", "dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react", "lint": "eslint src/", "type-check": "tsc --noEmit" }, "peerDependencies": { "react": "^18.0.0" }, "devDependencies": { "@repo/config-eslint": "workspace:*", "@repo/config-typescript": "workspace:*", "@types/react": "^18.2.0", "eslint": "^8.57.0", "tsup": "^8.0.0", "typescript": "^5.3.0" } }
import { ButtonHTMLAttributes, ReactNode } from 'react' interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { children: ReactNode variant?: 'primary' | 'secondary' } export function Button({ children, variant = 'primary', className = '', ...props }: ButtonProps) { const baseStyles = 'px-4 py-2 rounded font-medium transition-colors' const variantStyles = { primary: 'bg-blue-600 text-white hover:bg-blue-700', secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300' } return ( <button className={`${baseStyles} ${variantStyles[variant]} ${className}`} {...props} > {children} </button> ) }
export { Button } from './button' export { Card } from './card' export { Input } from './input'

Creating a Next.js App

{ "name": "web", "version": "0.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "type-check": "tsc --noEmit" }, "dependencies": { "@repo/ui": "workspace:*", "@repo/utils": "workspace:*", "next": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@repo/config-eslint": "workspace:*", "@repo/config-typescript": "workspace:*", "@types/node": "^20.11.0", "@types/react": "^18.2.0", "typescript": "^5.3.0" } }
import { Button } from '@repo/ui' import { formatDate } from '@repo/utils' export default function Home() { return ( <main className="p-8"> <h1 className="text-4xl font-bold mb-4"> Welcome to Turborepo </h1> <p className="mb-4"> Today is {formatDate(new Date())} </p> <Button onClick={() => alert('Hello!')}> Click me </Button> </main> ) }

Shared Configuration Packages

TypeScript Config

{ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "declaration": true, "declarationMap": true, "sourceMap": true, "composite": false, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true }, "exclude": ["node_modules", "dist"] }

ESLint Config

module.exports = { extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-explicit-any': 'warn' } }

Development Workflow

# Install all dependencies pnpm install # Run all apps in dev mode pnpm dev # Run only web app pnpm dev --filter=web # Build all apps and packages pnpm build # Build only UI package and its dependents pnpm build --filter=@repo/ui... # Run tests pnpm test # Lint all packages pnpm lint # Type check everything pnpm type-check

Remote Caching

Remote caching shares build artifacts across your team and CI/CD.

# Link to Vercel for remote caching npx turbo login npx turbo link
{ "remoteCache": { "signature": true } }

Now when a teammate or CI builds something you've already built locally, it'll use your cached result instead of rebuilding.

CI/CD Configuration

name: CI on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: version: 9.0.0 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm build env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: Test run: pnpm test - name: Lint run: pnpm lint

Performance Tips

  1. Use filtering - Only build what you need
  2. Enable remote caching - Share builds across team
  3. Optimize outputs - Only cache necessary files
  4. Leverage dependencies - Use dependsOn correctly
  5. Profile builds - Use turbo build --profile to find bottlenecks

Common Pitfalls

Issue: Circular dependencies

# Detect circular dependencies pnpm why @repo/ui

Issue: Cache invalidation

# Clear Turborepo cache rm -rf .turbo turbo build --force

Issue: TypeScript errors across packages

# Build packages in dependency order turbo build --filter=...@repo/ui

Conclusion

Turborepo provides a powerful foundation for monorepo development. Start with a simple structure, share common configurations, and leverage remote caching for maximum productivity. The incremental build system ensures your CI/CD stays fast as your codebase grows. With proper setup, you'll ship features faster while maintaining code quality across all your projects.

GitHub
LinkedIn
youtube