Introduction
As your startup grows, managing multiple projects (web app, mobile app, backend API, shared libraries) becomes challenging. A monorepo with Turborepo lets you share code between projects while maintaining independent deployments and fast builds. This guide shows you how to set it up.
Why Monorepo?
The Problem with Multiple Repos
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Multiple Repos โ Chaos โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ repo-web/ โ
โ โโโ utils/ โ
โ โโโ formatDate.ts (v1.0) โ
โ โ
โ repo-mobile/ โ
โ โโโ utils/ โ
โ โโโ formatDate.ts (v0.9) โ Out of sync! โ
โ โ
โ repo-api/ โ
โ โโโ utils/ โ
โ โโโ formatDate.ts (v1.0) โ
โ โ
โ โ Bug fixes don't propagate โ
โ โ Inconsistent code โ
โ โ Hard to refactor โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Monorepo Solution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Monorepo โ Organized โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ my-startup/ โ
โ โโโ apps/ โ
โ โ โโโ web/ โ Next.js โ
โ โ โโโ mobile/ โ React Native โ
โ โ โโโ api/ โ Express/Next.js API โ
โ โ โโโ admin/ โ Admin dashboard โ
โ โ โ
โ โโโ packages/ โ
โ โโโ ui/ โ Shared UI components โ
โ โโโ utils/ โ Shared utilities โ
โ โโโ config/ โ Shared configs โ
โ โโโ types/ โ Shared TypeScript types โ
โ โ
โ โ One source of truth โ
โ โ Easy refactoring โ
โ โ Shared dependencies โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Setting Up Turborepo
Initial Setup
# Create new monorepo
mkdir my-startup && cd my-startup
# Initialize
npm init -w
# Install turbo
npm install -D turbo
# Create turbo.json
echo '{"pipeline": {"build": {}, "dev": {}, "lint": {}}}' > turbo.json
Package Structure
{
"name": "my-startup",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
}
}
// apps/web/package.json
{
"name": "@my-startup/web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "next lint"
},
"dependencies": {
"@my-startup/ui": "*",
"@my-startup/utils": "*",
"next": "latest",
"react": "latest"
}
}
Turbo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"deploy": {
"dependsOn": ["build"],
"cache": false
}
}
}
Sharing Code Between Apps
Shared Utils Package
// packages/utils/src/index.ts
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)
}
export function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(' ')
}
export function truncate(str: string, length: number): string {
if (str.length <= length) return str
return str.slice(0, length) + '...'
}
// packages/utils/package.json
{
"name": "@my-startup/utils",
"version": "0.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./date": "./src/date.ts",
"./string": "./src/string.ts"
}
}
Shared UI Components
// packages/ui/src/Button.tsx
import { cn } from '@my-startup/utils'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost'
}
export function Button({
variant = 'primary',
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
'px-4 py-2 rounded-lg font-medium transition-colors',
variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
variant === 'secondary' && 'bg-gray-100 text-gray-900 hover:bg-gray-200',
variant === 'ghost' && 'bg-transparent hover:bg-gray-100',
className
)}
{...props}
>
{children}
</button>
)
}
Using Shared Packages
// apps/web/app/page.tsx
import { Button } from '@my-startup/ui'
import { formatDate, cn } from '@my-startup/utils'
export default function HomePage() {
return (
<div className={cn('p-8', 'max-w-2xl')}>
<h1 className="text-3xl font-bold">
Welcome {formatDate(new Date())}
</h1>
<Button variant="primary" onClick={() => console.log('clicked')}>
Get Started
</Button>
</div>
)
}
Build Optimization
Caching
// turbo.json - enable remote caching
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"cache": true
}
}
}
# Login to Vercel for remote cache (optional)
npx turbo login
npx turbo link
Parallel Execution
# Run multiple apps in parallel
turbo run dev --parallel
# Run specific app
turbo run dev --filter=web
# Run all except mobile
turbo run dev --filter=!mobile
Build Scripts
// root package.json
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean",
"typecheck": "turbo run typecheck"
}
}
Sharing Types
API Types
// packages/types/src/api.ts
export interface User {
id: string
email: string
name: string
createdAt: string
}
export interface CreateUserInput {
email: string
name: string
password: string
}
export interface ApiResponse<T> {
data?: T
error?: string
}
// Use in API (apps/api/src/routes/users.ts)
import { User, CreateUserInput, ApiResponse } from '@my-startup/types'
export async function createUser(input: CreateUserInput): Promise<ApiResponse<User>> {
// Implementation
}
// Use in Web (apps/web/app/users/page.tsx)
import { User, ApiResponse } from '@my-startup/types'
// Full type safety across apps!
Deployment
Deploying Individual Apps
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
- run: npm run build
working-directory: apps/web
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
working-directory: apps/web
Environment Variables
# .env
# Root - shared vars
DATABASE_URL=postgres://...
# App-specific in .env.{app}.local
# apps/web/.env.local
NEXT_PUBLIC_API_URL=http://localhost:3000
Common Pitfalls
1. Too Many Shared Packages
Wrong:
# Each tiny utility in its own package
packages/
utils/
date/
string/
array/
object/
number/
# Result: Over-engineered, slow installs
Correct:
# Group related code
packages/
utils/ # All utilities
ui/ # All UI components
config/ # All configs
# Start simple, extract when needed
2. Not Using Filters
Wrong:
# Rebuild everything for small change
npm run build
Correct:
# Rebuild only affected apps
turbo run build --filter=web
Key Takeaways
- Start monorepo early - Easier than migrating later
- Turborepo handles caching - Super fast builds
- Share types between apps - End-to-end type safety
- Deploy apps independently - Each app has its own CI/CD
- Keep it simple - Don’t over-engineer package structure
Comments