Scalabilité = architecture, pas infrastructure. Monorepo bien configuré = 10× faster builds.
Ce guide couvre monorepo Turborepo pour Next.js : setup complet, shared packages, remote caching, CI/CD optimization, migration monolith vers monorepo.
Résultat client : Build time -84% (22min → 3min 30s)
TL;DR : Monorepo Tools 2026
| Tool | Popularity | Build Speed | Learning Curve | Best For |
|---|---|---|---|---|
| Turborepo | ⭐⭐⭐⭐⭐ (Vercel) | 🚀 Fastest | ✅ Easy | 🏆 Next.js apps |
| Nx | ⭐⭐⭐⭐ | ⚡ Fast | ⚠️ Complex | Enterprise (Angular/React) |
| pnpm workspaces | ⭐⭐⭐ | ⚡ Medium | ✅ Simple | Basic monorepo |
| Lerna | ⭐⭐ (declining) | 🐌 Slow | ⚠️ Complex | Legacy (deprecated) |
| Rush | ⭐⭐ | ⚡ Fast | ⚠️ Steep | Microsoft ecosystem |
Recommendation 2026 :
- Turborepo : Next.js monorepo (rachat Vercel 2021, integration native)
- Nx : Large enterprise (100+ packages)
- pnpm workspaces : Simple projects (no build optimization needed)
1. Pourquoi Monorepo ?
Problems Monolith
Scenario typique :
my-saas/
├── web/ (Next.js customer app)
├── admin/ (Next.js admin dashboard)
├── mobile/ (React Native)
└── landing/ (Next.js marketing site)
Chaque repo séparé = duplication :
- ⚠️ Shared components copy-paste (Button, Modal, etc.)
- ⚠️ Types duplicated (User, Post, etc.)
- ⚠️ Utils duplicated (formatDate, validateEmail, etc.)
- ⚠️ Config duplicated (ESLint, TypeScript, Tailwind)
- ⚠️ Version drift (React 18.2 vs 18.3)
Maintenance nightmare : Bug fix dans Button → copy-paste 4 repos
Monorepo Benefits
✅ Single source of truth : Shared code = 1 place
✅ Atomic commits : Update Button + all apps in 1 commit
✅ TypeScript refactoring : Rename breaks all usages (fixable)
✅ Dependency management : Single lockfile
✅ CI/CD optimization : Build only changed packages
Real client gains :
- Code duplication : -67% (eliminated shared components copy-paste)
- Cross-repo refactoring : 3 days → 4 hours
- CI build time : 22min → 3min 30s (-84%)
2. Turborepo Setup Complete
Installation
# Create Turborepo from template
npx create-turbo@latest my-monorepo
# Options:
# - Package manager? → pnpm (recommended)
# - Include example apps? → Yes
Generated structure :
my-monorepo/
├── apps/
│ ├── web/ # Next.js customer app
│ └── docs/ # Next.js docs site
├── packages/
│ ├── ui/ # Shared React components
│ ├── typescript-config/ # Shared TS configs
│ └── eslint-config/ # Shared ESLint configs
├── package.json # Root package
├── pnpm-workspace.yaml
└── turbo.json # Turborepo config
Manual Setup (Custom Monorepo)
1. Initialize pnpm workspace :
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
2. Root package.json :
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
},
"engines": {
"node": ">=20",
"pnpm": ">=9"
},
"packageManager": "pnpm@9.0.0"
}
3. Turborepo config :
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}
3. Shared UI Package
Create packages/ui
mkdir -p packages/ui
cd packages/ui
pnpm init
Package structure :
packages/ui/
├── package.json
├── tsconfig.json
├── src/
│ ├── button.tsx
│ ├── input.tsx
│ ├── modal.tsx
│ └── index.ts
└── tailwind.config.ts
package.json :
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint . --max-warnings 0",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/react": "^18.2.0",
"eslint": "^8.57.0",
"typescript": "^5.4.0"
}
}
Button component :
// packages/ui/src/button.tsx
import { type ButtonHTMLAttributes, type ReactNode } from 'react'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
}
export function Button({ children, variant = 'primary', className = '', ...props }: ButtonProps) {
const baseStyles = 'px-4 py-2 rounded-lg 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',
danger: 'bg-red-600 text-white hover:bg-red-700',
}
return (
<button className={`${baseStyles} ${variantStyles[variant]} ${className}`} {...props}>
{children}
</button>
)
}
Export :
// packages/ui/src/index.ts
export { Button } from './button'
export { Input } from './input'
export { Modal } from './modal'
Use in Next.js App
Install package :
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@repo/ui": "workspace:*", // ✅ Internal package
"next": "15.0.0",
"react": "^18.2.0"
}
}
Use component :
// apps/web/app/page.tsx
import { Button } from '@repo/ui'
export default function HomePage() {
return (
<div>
<h1>Welcome</h1>
<Button variant="primary">Get Started</Button>
</div>
)
}
Tailwind config (important!) :
// apps/web/tailwind.config.ts
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
// ✅ Include UI package
'../../packages/ui/src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}
export default config
4. Shared TypeScript Config
Create packages/typescript-config
// packages/typescript-config/package.json
{
"name": "@repo/typescript-config",
"version": "0.0.0",
"private": true,
"files": ["base.json", "nextjs.json", "react-library.json"]
}
Base config :
// packages/typescript-config/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"noUncheckedIndexedAccess": true,
"noEmit": true
}
}
Next.js config :
// packages/typescript-config/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
React library config :
// packages/typescript-config/react-library.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"jsx": "react-jsx"
}
}
Usage :
// apps/web/tsconfig.json
{
"extends": "@repo/typescript-config/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
5. Remote Caching (Vercel)
Problem : Local Cache Only
Default Turborepo :
- ✅ Cache builds locally (
.turbo/cache/) - ❌ CI rebuilds from scratch every time
- ❌ Team members rebuild same code
Waste : 10 developers × 5min build = 50min wasted daily
Solution : Remote Cache
Vercel Remote Cache (gratuit) :
# Login Vercel
npx turbo login
# Link project
npx turbo link
What happens :
- Developer builds locally → Cached remotely
- CI pulls cache → Skip rebuild if code unchanged
- Other developers pull cache → Instant builds
Build time comparison :
| Scenario | No Cache | Local Cache | Remote Cache |
|---|---|---|---|
| First build | 5min 30s | 5min 30s | 5min 30s |
| Rebuild (no changes) | 5min 30s | 3s | 3s |
| CI build (no changes) | 5min 30s | 5min 30s | 3s ✅ |
CI/CD setup :
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build
run: pnpm turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Secrets setup :
- Vercel Dashboard → Settings → Tokens
- Create token
- GitHub repo → Settings → Secrets → Add
TURBO_TOKEN
6. Task Dependencies & Parallelization
Pipeline Configuration
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"], // ✅ Build dependencies first
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["^build", "lint"], // Test after build + lint
"outputs": ["coverage/**"]
},
"lint": {
// No dependencies (runs first)
},
"dev": {
"cache": false, // Never cache dev server
"persistent": true // Keep running
}
}
}
^build meaning : Build all workspace dependencies first
Example :
apps/web (depends on @repo/ui)
├── @repo/ui/build ✅ (runs first)
└── apps/web/build ✅ (runs after)
Parallel Execution
# Build all packages (parallel)
pnpm turbo build
# Output:
# • Packages in scope: @repo/ui, web, docs
# • Running build in 3 packages
# • @repo/ui:build: cache miss, executing...
# • web:build: waiting for dependencies...
# • docs:build: waiting for dependencies...
# • @repo/ui:build: finished (2.3s)
# • web:build: cache miss, executing... (parallel)
# • docs:build: cache miss, executing... (parallel)
# • web:build: finished (5.1s)
# • docs:build: finished (4.8s)
Speed gain : Sequential 12.2s → Parallel 5.1s (2.4× faster)
7. Environment Variables
Problem : Sharing .env
Bad approach :
apps/web/.env
apps/admin/.env (duplicate DATABASE_URL)
Solution : Root .env + package-specific
# Root .env (shared secrets)
DATABASE_URL=postgres://...
REDIS_URL=redis://...
# apps/web/.env (app-specific)
NEXT_PUBLIC_APP_NAME=My SaaS
NEXT_PUBLIC_API_URL=https://api.myapp.com
Turborepo config :
// turbo.json
{
"globalDependencies": ["**/.env.*local", ".env"]
}
Load in Next.js :
// apps/web/next.config.ts
const nextConfig = {
env: {
DATABASE_URL: process.env.DATABASE_URL, // ✅ From root .env
},
}
8. Versioning & Changesets
Install Changesets
pnpm add -Dw @changesets/cli
pnpm changeset init
Create changeset :
pnpm changeset
# Prompts:
# - Which packages changed? → @repo/ui
# - What type of change? → minor
# - Summary? → "Added Modal component"
Generated file :
# .changeset/sharp-lions-dance.md
---
## "@repo/ui": minor
Added Modal component with backdrop and close button
Version bump :
pnpm changeset version
# Result:
# packages/ui/package.json: 0.1.0 → 0.2.0
# packages/ui/CHANGELOG.md updated
Publish (if public packages) :
pnpm changeset publish
9. Migration Monolith → Monorepo
Step-by-Step Migration
Before :
my-app/ (single Next.js app)
├── components/
├── app/
└── package.json
After :
my-monorepo/
├── apps/
│ └── web/ (existing app)
├── packages/
│ └── ui/ (extracted shared components)
└── turbo.json
Migration Steps
1. Create monorepo structure :
mkdir my-monorepo
cd my-monorepo
# Initialize
pnpm init
pnpm add -D turbo
# Create workspace
echo "packages:\n - 'apps/*'\n - 'packages/*'" > pnpm-workspace.yaml
2. Move existing app :
mkdir -p apps
mv ../my-app apps/web
3. Extract shared components :
mkdir -p packages/ui/src
# Move components
mv apps/web/components/Button.tsx packages/ui/src/button.tsx
mv apps/web/components/Input.tsx packages/ui/src/input.tsx
# Create package.json
cd packages/ui
pnpm init -y
4. Update imports :
# Before
import { Button } from '@/components/Button'
# After
import { Button } from '@repo/ui'
5. Install dependencies :
cd apps/web
pnpm add @repo/ui@workspace:*
6. Configure Turborepo :
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
7. Test :
pnpm turbo dev
10. Best Practices
✅ DO
- Shared code → packages (ui, utils, config)
- App-specific code → apps
- Internal packages :
@repo/*naming - TypeScript strict : Catch cross-package errors
- Changesets : Track package versions
- Remote caching : Vercel/Nx Cloud
❌ DON'T
- Circular dependencies : packageA imports packageB imports packageA
- Duplicate dependencies : Install once at root (if possible)
- Large packages : Split ui → ui-marketing, ui-dashboard
- Monolith in monorepo : 1 giant package defeats purpose
11. Performance Benchmark
Test : Build 5 Next.js Apps + 3 Packages
| Tool | Cold Build | Warm Build (cache) | Parallel |
|---|---|---|---|
| Turborepo | 3min 42s | 4s | ✅ Yes |
| Nx | 4min 18s | 6s | ✅ Yes |
| pnpm workspaces | 8min 51s | 8min 51s | ❌ No |
| Separate repos | 22min 14s | 22min 14s | ❌ No |
Turborepo wins : Fastest + best caching
12. Troubleshooting
Error : "Module not found @repo/ui"
Solution : Install workspace dependency
cd apps/web
pnpm add @repo/ui@workspace:*
Error : "Tailwind classes not working"
Solution : Include package in content config
// apps/web/tailwind.config.ts
export default {
content: [
'./app/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.{ts,tsx}', // ✅ Add this
],
}
Build works locally but fails CI
Solution : Add remote caching
# .github/workflows/ci.yml
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Conclusion
Monorepo = scalability strategy, not hype.
Setup recommandé 2026 :
- Turborepo : Build orchestration
- pnpm : Package manager (fast, efficient)
- Changesets : Versioning (if publishing packages)
- Vercel Remote Cache : CI/CD optimization
Migration time estimate :
- Small app (1-2 apps) : 1-2 days
- Medium (3-5 apps) : 1 week
- Large (10+ apps) : 2-3 weeks
ROI :
- Build time : -84% (22min → 3min 30s)
- Code duplication : -67%
- Refactoring time : -85% (3 days → 4h)
Worth it ? ✅ Yes si 2+ apps with shared code
FAQ
Turborepo vs Nx ?
Turborepo : Simple, Next.js-optimized, Vercel integration. Nx : Enterprise features (code generators, dependency graph visualization). Choose Turbo unless need Nx advanced features.
Monorepo = Vercel required ?
❌ Non. Turborepo works anywhere (GitHub Actions, GitLab CI, etc.). Vercel Remote Cache gratuit bonus si deployed Vercel.
How many packages typical monorepo ?
Starter : 2-3 (ui, config, utils). Production : 5-10 (ui, api-client, email-templates, etc.). Enterprise : 50+ (domain-driven packages).
pnpm required ?
⚠️ Recommended but not required. Turborepo works with npm/yarn/pnpm. pnpm fastest + most efficient (shared node_modules).
Articles connexes :