flowchart TD
subgraph Monorepo["📦 Monorepo Root"]
subgraph Apps["apps/"]
Web["🌐 web"]
Desktop["🖥️ desktop"]
Mobile["📱 mobile"]
Sidecar["🐍 sidecar"]
end
subgraph Packages["packages/"]
UI["🎨 ui"]
Shared["📋 shared"]
Config["⚙️ config"]
end
end
Web --> UI
Web --> Shared
Desktop --> UI
Desktop --> Shared
Mobile --> UI
Mobile --> Shared
Sidecar -.-> Shared
style UI fill:#61dafb
style Shared fill:#3178c6
Monorepo Architecture: Scaling Beyond a Single App
As your React project grows, you often find yourself with multiple related codebases: a web app, a desktop app, a shared component library, and maybe a backend server. Managing these as separate repositories leads to dependency hell and endless synchronization issues.
The solution? A Monorepo.
Why Monorepo?
| Challenge | Multi-Repo | Monorepo |
|---|---|---|
| Shared code | Publish to npm, version dance | Just import it |
| Type safety | Hope types match | Guaranteed by TypeScript |
| Atomic changes | Multiple PRs, coordination | Single PR, single review |
| CI/CD | N pipelines to maintain | One pipeline, smart caching |
| Onboarding | Clone N repos, configure each | Clone once, run once |
Project Structure
my-project/
├── apps/
│ ├── web/ # Next.js or Vite
│ │ ├── src/
│ │ ├── package.json
│ │ └── vite.config.ts
│ ├── desktop/ # Tauri + React
│ │ ├── src/
│ │ ├── src-tauri/
│ │ └── package.json
│ └── sidecar/ # Python AI backend
│ ├── server.py
│ ├── requirements.txt
│ └── pyproject.toml
├── packages/
│ ├── ui/ # Shared React components
│ │ ├── src/
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── index.ts
│ │ └── package.json
│ ├── shared/ # Types, schemas, utilities
│ │ ├── src/
│ │ │ ├── schemas/
│ │ │ ├── utils/
│ │ │ └── index.ts
│ │ └── package.json
│ └── config/ # Shared configs
│ ├── eslint/
│ ├── tsconfig/
│ └── package.json
├── package.json # Root workspace config
├── pnpm-workspace.yaml # Workspace definition
└── turbo.json # Build orchestration
Setting Up Workspaces
pnpm (Recommended)
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'// package.json (root)
{
"name": "my-project",
"private": true,
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^2.0.0"
}
}npm/yarn Workspaces
// package.json (root)
{
"name": "my-project",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}Turborepo: Smart Build Orchestration
Turborepo understands your dependency graph and: - Only rebuilds what changed - Caches build outputs - Runs tasks in parallel
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}Filtering
# Run dev for web app only
pnpm turbo dev --filter=@myproject/web
# Run build for everything web depends on
pnpm turbo build --filter=@myproject/web...
# Run tests for changed packages only
pnpm turbo test --filter=[HEAD^1]CI/CD with Caching
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # For turbo diff detection
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Turbo Cache
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Build
run: pnpm build
- name: Lint
run: pnpm lint
- name: Test
run: pnpm testCommon Patterns
Internal Package Versioning
For private monorepos, use workspace:* instead of version numbers:
{
"dependencies": {
"@myproject/shared": "workspace:*"
}
}This means “use whatever version is in the workspace.”
Path Aliases
Configure bundlers to resolve workspace packages:
// apps/web/vite.config.ts
export default defineConfig({
resolve: {
alias: {
'@myproject/ui': path.resolve(__dirname, '../../packages/ui/src'),
'@myproject/shared': path.resolve(__dirname, '../../packages/shared/src'),
},
},
});Development Workflow
# Install all dependencies
pnpm install
# Start everything in dev mode
pnpm dev
# Or start specific apps
pnpm --filter @myproject/web dev
pnpm --filter @myproject/desktop devMonorepo Benefits Recap
TipWhy This Approach Wins
- Single Source of Truth — Define types and utilities once
- Atomic Commits — Change API and UI in a single PR
- Unified Tooling — One
pnpm installfor everything - Smart Caching — Turborepo only rebuilds what changed
- Consistent Standards — Shared ESLint, TypeScript, Prettier configs
In the next chapter, we’ll see how to wrap one of these apps in a native shell using Tauri and create cross-platform desktop applications.