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 firstoutputs- Define what to cachecache: false- Disable caching for dev serverspersistent: 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
- Use filtering - Only build what you need
- Enable remote caching - Share builds across team
- Optimize outputs - Only cache necessary files
- Leverage dependencies - Use
dependsOncorrectly - Profile builds - Use
turbo build --profileto 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.