Monorepo Turborepo Next.js : Guide Complet 2026

Brandon Sueur17 min

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 :

  1. Developer builds locally → Cached remotely
  2. CI pulls cache → Skip rebuild if code unchanged
  3. 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 :

  1. Vercel Dashboard → Settings → Tokens
  2. Create token
  3. 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 :