Cours Next.js · Module 9: Intégrations et fonctions avancées

Monorepo avec Turborepo - Mettre à l'échelle les projets Next.js

6 min de lecture
Dans cette leçon8

Dans la vaste infrastructure de la Métropole Quantum 2150, où des dizaines de microservices et d'applications frontend collaborent entre eux, tu as besoin d'un outil pour gérer le code à grande échelle. Turborepo est un système de build pour monorepo qui fonctionne avec Next.js comme un processeur quantique précis - rapidement, efficacement et de manière fiable.

Pourquoi un monorepo pour Next.js ?

Un monorepo est une stratégie consistant à stocker plusieurs projets dans un seul dépôt. Pour l'écosystème Next.js, cela apporte d'énormes avantages :

  • Partage de code - composants UI, configurations, types utilisés dans plusieurs applications
  • Changements atomiques - une modification dans une bibliothèque partagée est immédiatement visible partout
  • Outillage cohérent - une seule configuration ESLint, TypeScript, Prettier
  • CI/CD simplifié - un seul pipeline pour tout l'écosystème
  • Meilleure collaboration - tout au même endroit, revue de code facilitée

Configuration de Turborepo

Initialisation du monorepo

1# Crée un nouveau monorepo avec Turborepo
2npx create-turbo@latest quantum-monorepo
3
4# Ou ajoute Turborepo à un projet existant
5npm install turbo --save-dev

Structure des dossiers

1quantum-monorepo/
2  apps/
3    web/                 # Application Next.js principale
4      package.json
5      next.config.js
6    admin/               # Panneau d'administration Next.js
7      package.json
8      next.config.js
9    docs/                # Documentation Next.js
10      package.json
11
12  packages/
13    ui/                  # Composants React partagés
14      package.json
15      src/
16        Button.tsx
17        Card.tsx
18        index.ts
19    config-eslint/       # Configuration ESLint partagée
20      package.json
21    config-typescript/   # Configuration TypeScript partagée
22      package.json
23    types/               # Interfaces TypeScript partagées
24      package.json
25      src/
26        user.ts
27        product.ts
28
29  turbo.json             # Configuration de Turborepo
30  package.json           # package.json racine

Configuration de turbo.json

1{
2  "$schema": "https://turbo.build/schema.json",
3  "globalDependencies": ["**/.env.*local"],
4  "pipeline": {
5    "build": {
6      "dependsOn": ["^build"],
7      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
8    },
9    "lint": {
10      "dependsOn": ["^build"]
11    },
12    "dev": {
13      "cache": false,
14      "persistent": true
15    },
16    "test": {
17      "dependsOn": ["^build"]
18    },
19    "type-check": {
20      "dependsOn": ["^build"]
21    }
22  }
23}

Éléments clés de la configuration :

  • dependsOn: ["^build"] - construis les dépendances avant de construire le package
  • outputs - ce qu'il faut mettre en cache (dossiers de sortie)
  • cache: false - le serveur de dev ne doit pas être mis en cache
  • persistent: true - processus de longue durée (serveur de dev)

package.json racine

1{
2  "name": "quantum-monorepo",
3  "private": true,
4  "workspaces": ["apps/*", "packages/*"],
5  "scripts": {
6    "build": "turbo build",
7    "dev": "turbo dev",
8    "lint": "turbo lint",
9    "test": "turbo test",
10    "type-check": "turbo type-check"
11  },
12  "devDependencies": {
13    "turbo": "^2.0.0"
14  }
15}

Packages partagés

Package UI - composants React

1// packages/ui/package.json
2{
3  "name": "@quantum/ui",
4  "version": "0.1.0",
5  "main": "./src/index.ts",
6  "types": "./src/index.ts",
7  "dependencies": {
8    "react": "^19.2.0"
9  },
10  "devDependencies": {
11    "@quantum/config-typescript": "workspace:*",
12    "typescript": "^5.5.0"
13  }
14}
1// packages/ui/src/Button.tsx
2import React from 'react';
3
4interface ButtonProps {
5  children: React.ReactNode;
6  variant?: 'primary' | 'secondary' | 'danger';
7  onClick?: () => void;
8  disabled?: boolean;
9}
10
11export function Button({ children, variant = 'primary', onClick, disabled }: ButtonProps) {
12  const styles = {
13    primary: { background: '#7c4dff', color: '#fff' },
14    secondary: { background: '#64ffda', color: '#000' },
15    danger: { background: '#f44336', color: '#fff' },
16  };
17
18  return (
19    <button
20      onClick={onClick}
21      disabled={disabled}
22      style={{
23        ...styles[variant],
24        padding: '10px 24px',
25        borderRadius: '8px',
26        border: 'none',
27        cursor: disabled ? 'not-allowed' : 'pointer',
28        opacity: disabled ? 0.5 : 1,
29      }}
30    >
31      {children}
32    </button>
33  );
34}
1// packages/ui/src/index.ts
2export { Button } from './Button';
3export { Card } from './Card';

Package types - interfaces partagées

1// packages/types/src/user.ts
2export interface User {
3  id: string;
4  email: string;
5  name: string;
6  role: 'admin' | 'user' | 'moderator';
7  createdAt: string;
8}
1// packages/types/src/index.ts
2export * from './user';
3export * from './product';

Workspace dependencies

Pour utiliser un package partagé dans une application Next.js :

1// apps/web/package.json
2{
3  "name": "@quantum/web",
4  "dependencies": {
5    "@quantum/ui": "workspace:*",
6    "@quantum/types": "workspace:*",
7    "next": "^16.0.0",
8    "react": "^19.2.0",
9    "react-dom": "^19.2.0"
10  },
11  "devDependencies": {
12    "@quantum/config-typescript": "workspace:*",
13    "@quantum/config-eslint": "workspace:*"
14  }
15}

Utilisation dans le code de l'application :

1// apps/web/app/page.tsx
2import { Button, Card } from '@quantum/ui';
3import type { Product } from '@quantum/types';
4
5export default async function HomePage() {
6  return (
7    <div>
8      <h1>Quantum Store</h1>
9      <Button variant="primary">Ajouter au panier</Button>
10    </div>
11  );
12}

Cache et pipeline de Turborepo

Turborepo utilise un système de cache intelligent qui mémorise les résultats de build et ignore les packages inchangés.

Comment fonctionne le cache

1# Premier build - tout à partir de zéro
2npx turbo build
3# Output: 5 tasks, 0 cached, 5 not cached
4# Temps : 45s
5
6# Deuxième build (sans changement) - cache complet
7npx turbo build
8# Output: 5 tasks, 5 cached, 0 not cached
9# Temps : 0,3s

Remote Caching avec Vercel

1# Connecte-toi au Remote Cache de Vercel
2npx turbo login
3npx turbo link
4
5# Maintenant, le cache est partagé entre les développeurs et la CI

Filtrer le build

1# Construis uniquement une application spécifique
2npx turbo build --filter=@quantum/web
3
4# Construis l'application et ses dépendances
5npx turbo build --filter=@quantum/web...
6
7# Construis uniquement ce qui a changé depuis le dernier commit
8npx turbo build --filter=[HEAD^1]

Déployer un monorepo sur Vercel

Configuration de next.config.js pour un monorepo

1// apps/web/next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4  transpilePackages: ['@quantum/ui'],
5  output: 'standalone',
6};
7
8module.exports = nextConfig;

L'option transpilePackages indique à Next.js de transpiler le code des packages du monorepo (par défaut, Next.js ne transpile pas node_modules).

Déploiement multi-applications

Vercel ne construit automatiquement que les applications modifiées. Si tu modifies packages/ui, les applications qui dépendent de ce package seront reconstruites, tandis que les autres seront ignorées.

Bonnes pratiques

1. Conventions de nommage des packages

Utilise un scope (@nom-organisation/) pour tous les packages et applications :

1@quantum/ui
2@quantum/types
3@quantum/config-eslint
4@quantum/web
5@quantum/admin

2. Versioning

Utilise workspace:* pour les dépendances internes - ne code jamais les versions en dur :

1"@quantum/ui": "workspace:*"

3. Gestion des variables d'environnement

Chaque application a son propre fichier .env, et turbo.json déclare les dépendances globales vis-à-vis des fichiers d'environnement.

Résumé

Turborepo est un outil puissant pour mettre à l'échelle les projets Next.js dans une architecture monorepo :

  1. Packages partagés - UI, types, configurations utilisés dans plusieurs applications
  2. Cache intelligent - ignore les packages inchangés, accélère les builds
  3. Pipeline - dépendances entre les tâches, build en parallèle
  4. Dépendances de workspace - workspace:* pour les packages internes
  5. Remote caching - cache partagé entre les développeurs et la CI
  6. Déploiement sur Vercel - support natif, détection automatique des changements

Dans la Métropole Quantum 2150, où chaque projet grandit et doit être mis à l'échelle, Turborepo est le fondement de l'architecture - il permet de gérer des dizaines d'applications et de bibliothèques depuis un seul endroit.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3interface Package {
4  name: string;
5  type: 'app' | 'package';
6  status: 'cached' | 'building' | 'built' | 'pending';
7  duration?: number;
8  deps: string[];
9}
10
11const TurborepoDemo = () => {
12  const [packages, setPackages] = useState<Package[]>([
13    { name: '@quantum/web', type: 'app', status: 'pending', deps: ['@quantum/ui', '@quantum/types'] },
14    { name: '@quantum/admin', type: 'app', status: 'pending', deps: ['@quantum/ui', '@quantum/types'] },
15    { name: '@quantum/docs', type: 'app', status: 'pending', deps: [] },
16    { name: '@quantum/ui', type: 'package', status: 'pending', deps: ['@quantum/types'] },
17    { name: '@quantum/types', type: 'package', status: 'pending', deps: [] },
18    { name: '@quantum/config-ts', type: 'package', status: 'pending', deps: [] },
19    { name: '@quantum/config-eslint', type: 'package', status: 'pending', deps: [] },
20  ]);
21  const [running, setRunning] = useState(false);
22  const [logs, setLogs] = useState<string[]>([]);
23  const [cacheEnabled, setCacheEnabled] = useState(true);
24  const [buildCount, setBuildCount] = useState(0);
25  const [changedPkg, setChangedPkg] = useState<string | null>(null);
26
27  const addLog = (msg: string) => setLogs(prev => [...prev.slice(-12), msg]);
28  const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
29
30  const statusIcon = (s: string) => {
31    switch (s) {
32      case 'built': case 'cached': return '✅';
33      case 'building': return '⏳';
34      default: return '⚪';
35    }
36  };
37
38  const statusColor = (s: string) => {
39    switch (s) {
40      case 'built': return '#4caf50';
41      case 'cached': return '#64ffda';
42      case 'building': return '#ff9800';
43      default: return '#555';
44    }
45  };
46
47  const runBuild = async () => {
48    setRunning(true);
49    setLogs([]);
50    setPackages(prev => prev.map(p => ({ ...p, status: 'pending', duration: undefined })));
51    setBuildCount(c => c + 1);
52    const isSecondBuild = buildCount > 0;
53
54    addLog('$ npx turbo build' + (changedPkg ? ' --filter=' + changedPkg + '...' : ''));
55
56    // Build d'abord les packages (ceux sans dépendances en premier)
57    const noDeps = packages.filter(p => p.type === 'package' && p.deps.length === 0);
58    for (const pkg of noDeps) {
59      const isCached = isSecondBuild && cacheEnabled && pkg.name !== changedPkg;
60      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
61      addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
62      await sleep(isCached ? 100 : 300);
63      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 2 } : p));
64    }
65
66    // Puis les packages avec dépendances
67    const withDeps = packages.filter(p => p.type === 'package' && p.deps.length > 0);
68    for (const pkg of withDeps) {
69      const isCached = isSecondBuild && cacheEnabled && pkg.name !== changedPkg && !pkg.deps.includes(changedPkg || '');
70      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
71      addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
72      await sleep(isCached ? 100 : 400);
73      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 5 } : p));
74    }
75
76    // Puis les apps
77    const apps = packages.filter(p => p.type === 'app');
78    for (const pkg of apps) {
79      const affectedByChange = changedPkg && (pkg.deps.includes(changedPkg) || pkg.name === changedPkg);
80      const isCached = isSecondBuild && cacheEnabled && !affectedByChange;
81      if (changedPkg && !affectedByChange && pkg.name !== changedPkg) {
82        setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'cached', duration: 0 } : p));
83        addLog('SKIP ' + pkg.name + ' (not affected)');
84        await sleep(80);
85        continue;
86      }
87      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
88      addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
89      await sleep(isCached ? 150 : 600);
90      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 15 } : p));
91    }
92
93    const cached = packages.filter(() => true).length; // placeholder
94    addLog('Build terminé !');
95    setRunning(false);
96    setChangedPkg(null);
97  };
98
99  const simulateChange = (pkgName: string) => {
100    setChangedPkg(pkgName);
101    addLog('Modifié : ' + pkgName);
102  };
103
104  const reset = () => {
105    setPackages(prev => prev.map(p => ({ ...p, status: 'pending', duration: undefined })));
106    setLogs([]);
107    setBuildCount(0);
108    setChangedPkg(null);
109  };
110
111  return (
112    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
113      <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Monorepo z Turborepo</h1>
114      <p style={{ textAlign: 'center', color: '#888', marginBottom: '20px' }}>Packages partagés + Cache intelligent + Pipeline</p>
115
116      <div style={{ maxWidth: '900px', margin: '0 auto' }}>
117        <div style={{ display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '16px', flexWrap: 'wrap' }}>
118          <button onClick={runBuild} disabled={running}
119            style={{ padding: '8px 20px', background: running ? '#555' : '#4caf50', border: 'none', borderRadius: '6px', color: '#fff', cursor: running ? 'default' : 'pointer', fontWeight: 'bold' }}>
120            {running ? 'Build en cours...' : 'turbo build'}
121          </button>
122          <button onClick={reset} disabled={running}
123            style={{ padding: '8px 20px', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
124            Réinitialiser
125          </button>
126          <label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
127            <input type="checkbox" checked={cacheEnabled} onChange={() => setCacheEnabled(!cacheEnabled)} />
128            <span style={{ color: cacheEnabled ? '#64ffda' : '#666', fontSize: '13px' }}>Cache distant</span>
129          </label>
130        </div>
131
132        {buildCount > 0 && !running && (
133          <div style={{ display: 'flex', gap: '6px', justifyContent: 'center', marginBottom: '12px', flexWrap: 'wrap' }}>
134            <span style={{ color: '#888', fontSize: '12px', padding: '4px 0' }}>Simuler un changement dans :</span>
135            {packages.filter(p => p.type === 'package').map(p => (
136              <button key={p.name} onClick={() => simulateChange(p.name)}
137                style={{ padding: '4px 10px', background: changedPkg === p.name ? '#ff9800' : 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '4px', color: '#fff', cursor: 'pointer', fontSize: '11px' }}>
138                {p.name.replace('@quantum/', '')}
139              </button>
140            ))}
141          </div>
142        )}
143
144        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
145          <div>
146            <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Packages du workspace</h3>
147            <div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '10px', overflow: 'hidden' }}>
148              {packages.map(pkg => (
149                <div key={pkg.name} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 12px', borderBottom: '1px solid #1a1a2e' }}>
150                  <span>{statusIcon(pkg.status)}</span>
151                  <span style={{ background: pkg.type === 'app' ? '#7c4dff33' : '#64ffda33', color: pkg.type === 'app' ? '#7c4dff' : '#64ffda', padding: '2px 6px', borderRadius: '4px', fontSize: '10px' }}>
152                    {pkg.type}
153                  </span>
154                  <span style={{ flex: 1, color: statusColor(pkg.status), fontSize: '13px' }}>{pkg.name}</span>
155                  {pkg.status === 'cached' && <span style={{ color: '#64ffda', fontSize: '10px' }}>CACHED</span>}
156                  {pkg.duration !== undefined && pkg.duration > 0 && <span style={{ color: '#555', fontSize: '11px' }}>{pkg.duration}s</span>}
157                </div>
158              ))}
159            </div>
160
161            <div style={{ marginTop: '12px', background: 'rgba(255,255,255,0.05)', padding: '12px', borderRadius: '8px' }}>
162              <h4 style={{ color: '#64ffda', fontSize: '12px', marginBottom: '8px' }}>Graphe de dépendances</h4>
163              <div style={{ fontSize: '11px', fontFamily: 'monospace', color: '#888' }}>
164                <div>@quantum/web ── @quantum/ui ── @quantum/types</div>
165                <div>@quantum/admin ── @quantum/ui ── @quantum/types</div>
166                <div>@quantum/docs (aucune dépendance interne)</div>
167              </div>
168            </div>
169          </div>
170
171          <div>
172            <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Journal de build</h3>
173            <div style={{ background: '#000', padding: '12px', borderRadius: '8px', minHeight: '300px', maxHeight: '400px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '11px' }}>
174              {logs.length === 0 ? (
175                <span style={{ color: '#555' }}>Clique sur « turbo build » pour démarrer...</span>
176              ) : logs.map((log, i) => (
177                <div key={i} style={{ color: log.includes('CACHE') ? '#64ffda' : log.includes('SKIP') ? '#888' : log.includes('Building') ? '#ff9800' : '#0f0', padding: '2px 0' }}>
178                  {log}
179                </div>
180              ))}
181            </div>
182          </div>
183        </div>
184      </div>
185    </div>
186  );
187};
188
189export default TurborepoDemo;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelle affirmation décrit le mieux le principal avantage de Turborepo dans les projets Next.js ?

Tâches pratiques dans le jeu

  • Tri vertical

    Remets dans l'ordre la construction des packages dans un monorepo Turborepo (des packages sans dépendances jusqu'aux applications)

Articles utiles