Construire un monorepo de production avec Turborepo, Next.js et des packages partages

Un seul repo pour les gouverner tous. Turborepo est le systeme de build haute performance pour les monorepos JavaScript et TypeScript. Dans ce tutoriel, vous allez construire un monorepo de qualite production avec plusieurs applications Next.js, des composants UI partages, une configuration TypeScript partagee et un cache distant pour des builds CI ultra-rapides.
Ce que vous allez apprendre
A la fin de ce tutoriel, vous serez capable de :
- Mettre en place un monorepo Turborepo avec des workspaces pnpm
- Creer plusieurs applications Next.js qui partagent du code
- Construire une bibliotheque de composants UI partages en TypeScript
- Configurer un package de configuration TypeScript partage
- Creer un package utilitaire partage utilise dans toutes les applications
- Mettre en place des pipelines Turborepo pour une orchestration efficace des taches
- Activer le cache distant pour des builds CI quasi instantanes
- Deployer des applications individuelles avec des workflows CI/CD adaptes
Prerequis
Avant de commencer, assurez-vous de disposer de :
- Node.js 20+ installe (
node --version) - pnpm 9+ installe (
pnpm --version) — sinon :npm install -g pnpm - Une experience en TypeScript (types, generiques, resolution de modules)
- Une familiarite avec Next.js (App Router, Server Components)
- Un editeur de code — VS Code ou Cursor recommande
Pourquoi Turborepo ?
A mesure que les projets grandissent, on se retrouve souvent avec plusieurs applications (site marketing, tableau de bord, panneau admin) et du code partage (composants UI, utilitaires, configurations). Deux options se presentent :
- Multi-repo : Des depots separes pour chaque package. La publication, la gestion des versions et la synchronisation des dependances deviennent penibles.
- Monorepo : Tout le code dans un seul depot avec un outillage partage. Le partage de code est plus facile, les modifications atomiques entre packages sont possibles, et le CI/CD est unifie.
Turborepo rend les monorepos viables en resolvant leur plus gros probleme : la performance des builds. Il offre :
| Fonctionnalite | Avantage |
|---|---|
| Hachage des taches | Ne reconstruit que ce qui a change |
| Execution parallele | Execute les taches independantes simultanement |
| Cache distant | Partage les artefacts de build entre equipe et CI |
| Orchestration de pipeline | Respecte automatiquement les dependances |
| Adoption incrementale | Ajout possible aux depots existants sans reecriture |
Compare aux alternatives comme Nx, Turborepo est plus leger, zero-config par defaut et concu specifiquement pour l'ecosysteme JavaScript.
Etape 1 : Creer la structure du monorepo
Creez un nouveau projet Turborepo avec le starter officiel :
pnpm dlx create-turbo@latest my-monorepoQuand vous serez invite a choisir :
- Selectionnez pnpm comme gestionnaire de packages
- Choisissez le starter default
Naviguez dans le projet :
cd my-monorepoVous verrez cette structure :
my-monorepo/
├── apps/
│ ├── web/ # Next.js app
│ └── docs/ # Next.js docs app
├── packages/
│ ├── ui/ # Shared UI components
│ ├── eslint-config/ # Shared ESLint config
│ └── typescript-config/ # Shared tsconfig
├── turbo.json # Turborepo pipeline config
├── pnpm-workspace.yaml
└── package.json
Examinons chaque element avant de le personnaliser.
Etape 2 : Comprendre la structure des workspaces
Le package.json racine
Le package.json racine definit les scripts au niveau du workspace et les dependances de developpement :
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"prettier": "^3.4.2",
"turbo": "^2.4.4",
"typescript": "^5.7.3"
},
"packageManager": "pnpm@9.15.4"
}A noter : turbo run build ne construit rien lui-meme. Il orchestre le script build de chaque package du workspace, en respectant les dependances et en mettant en cache les resultats.
pnpm-workspace.yaml
Ce fichier indique a pnpm ou trouver les packages du workspace :
packages:
- "apps/*"
- "packages/*"turbo.json
Voici le coeur de Turborepo — la configuration du pipeline :
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Concepts cles :
dependsOn: ["^build"]— Le^signifie "executer cette tache dans les dependances en premier." Siwebdepend deui, alorsui#buildest execute avantweb#build.outputs— Les fichiers que Turborepo doit mettre en cache. Lors des executions suivantes, si les entrees n'ont pas change, Turborepo rejoue la sortie mise en cache instantanement.cache: false— Les serveurs de developpement ne doivent jamais etre mis en cache.persistent: true— Identifie les taches longues (comme les serveurs de dev) qui ne se terminent pas.
Etape 3 : Creer une bibliotheque de composants UI partages
Le starter inclut un package ui basique. Ameliorons-le pour en faire une veritable bibliotheque de composants.
Configurer le package
Editez packages/ui/package.json :
{
"name": "@repo/ui",
"version": "0.0.1",
"private": true,
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./input": "./src/input.tsx",
"./badge": "./src/badge.tsx",
"./dialog": "./src/dialog.tsx"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"typescript": "^5.7.3"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Le champ exports est essentiel — il definit l'API publique de votre package. Au lieu d'un seul export barrel, chaque composant obtient son propre point d'entree. Cela permet le tree-shaking pour que les composants inutilises ne soient pas inclus dans le bundle.
Creer le composant Button
Creez packages/ui/src/button.tsx :
import { type ButtonHTMLAttributes, forwardRef } from "react";
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary:
"bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
outline:
"border-2 border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500",
ghost:
"text-gray-700 hover:bg-gray-100 focus:ring-gray-500",
danger:
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
};
const sizeStyles: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", size = "md", isLoading, className = "", children, disabled, ...props }, ref) => {
return (
<button
ref={ref}
className={`inline-flex items-center justify-center rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading && (
<svg
className="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
);
}
);
Button.displayName = "Button";
export { Button, type ButtonProps };Creer le composant Card
Creez packages/ui/src/card.tsx :
import type { HTMLAttributes, ReactNode } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
function Card({ children, className = "", ...props }: CardProps) {
return (
<div
className={`rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md ${className}`}
{...props}
>
{children}
</div>
);
}
function CardHeader({ children, className = "", ...props }: CardProps) {
return (
<div className={`mb-4 ${className}`} {...props}>
{children}
</div>
);
}
function CardTitle({ children, className = "", ...props }: CardProps) {
return (
<h3 className={`text-lg font-semibold text-gray-900 ${className}`} {...props}>
{children}
</h3>
);
}
function CardContent({ children, className = "", ...props }: CardProps) {
return (
<div className={`text-gray-600 ${className}`} {...props}>
{children}
</div>
);
}
export { Card, CardHeader, CardTitle, CardContent };Creer le composant Input
Creez packages/ui/src/input.tsx :
import { type InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className = "", id, ...props }, ref) => {
const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
return (
<div className="space-y-1">
{label && (
<label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
<input
ref={ref}
id={inputId}
className={`block w-full rounded-lg border px-3 py-2 text-gray-900 shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 ${
error
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
} ${className}`}
{...props}
/>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}
);
Input.displayName = "Input";
export { Input, type InputProps };Creer le composant Badge
Creez packages/ui/src/badge.tsx :
import type { HTMLAttributes } from "react";
type BadgeVariant = "default" | "success" | "warning" | "error" | "info";
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: BadgeVariant;
}
const variantStyles: Record<BadgeVariant, string> = {
default: "bg-gray-100 text-gray-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
error: "bg-red-100 text-red-800",
info: "bg-blue-100 text-blue-800",
};
function Badge({ variant = "default", className = "", children, ...props }: BadgeProps) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${variantStyles[variant]} ${className}`}
{...props}
>
{children}
</span>
);
}
export { Badge, type BadgeProps };Etape 4 : Creer un package utilitaire partage
Les vrais monorepos partagent plus que des composants. Creez un package utilitaire pour la logique partagee.
Initialiser le package
mkdir -p packages/utils/srcCreez packages/utils/package.json :
{
"name": "@repo/utils",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./cn": "./src/cn.ts",
"./format": "./src/format.ts",
"./validators": "./src/validators.ts"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"typescript": "^5.7.3"
}
}Creer les fonctions utilitaires
Creez packages/utils/src/cn.ts — le classique fusionneur de noms de classes :
type ClassValue = string | number | boolean | undefined | null | ClassValue[];
export function cn(...inputs: ClassValue[]): string {
return inputs
.flat(Infinity)
.filter((x) => typeof x === "string" && x.length > 0)
.join(" ");
}Creez packages/utils/src/format.ts :
export function formatCurrency(amount: number, currency = "USD", locale = "en-US"): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(amount);
}
export function formatDate(date: Date | string, locale = "en-US"): string {
const d = typeof date === "string" ? new Date(date) : date;
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
}).format(d);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_]+/g, "-")
.replace(/^-+|-+$/g, "");
}Creez packages/utils/src/validators.ts :
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function isNotEmpty(value: string): boolean {
return value.trim().length > 0;
}
export function isWithinLength(value: string, min: number, max: number): boolean {
const len = value.trim().length;
return len >= min && len <= max;
}Creez packages/utils/src/index.ts :
export { cn } from "./cn";
export { formatCurrency, formatDate, slugify } from "./format";
export { isValidEmail, isNotEmpty, isWithinLength } from "./validators";Ajoutez un tsconfig.json dans packages/utils/tsconfig.json :
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}Etape 5 : Configurer l'application web pour utiliser les packages partages
Connectons maintenant les packages partages a votre application Next.js.
Ajouter les dependances
Dans apps/web/package.json, ajoutez les dependances du workspace :
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Executez pnpm install depuis la racine pour tout lier :
pnpm installConfigurer Next.js pour le monorepo
Next.js doit connaitre les packages situes en dehors de son repertoire. Editez apps/web/next.config.ts :
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@repo/ui", "@repo/utils"],
};
export default nextConfig;L'option transpilePackages indique a Next.js de compiler ces packages du workspace via son bundler, vous permettant ainsi d'ecrire du TypeScript directement sans etape de build separee.
Utiliser les composants partages
Utilisez maintenant les packages partages dans votre application. Editez apps/web/app/page.tsx :
import { Button } from "@repo/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@repo/ui/card";
import { Input } from "@repo/ui/input";
import { Badge } from "@repo/ui/badge";
import { formatDate } from "@repo/utils/format";
export default function Home() {
return (
<main className="mx-auto max-w-4xl p-8">
<div className="mb-8">
<h1 className="text-4xl font-bold text-gray-900">
Turborepo Monorepo
</h1>
<p className="mt-2 text-lg text-gray-600">
Built with shared packages — {formatDate(new Date())}
</p>
</div>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Shared UI Components</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4">
These components come from <code>@repo/ui</code>:
</p>
<div className="flex flex-wrap gap-2">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Badges & Inputs</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-4 flex gap-2">
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Failed</Badge>
<Badge variant="info">Info</Badge>
</div>
<Input label="Email" placeholder="you@example.com" />
</CardContent>
</Card>
</div>
</main>
);
}Lancez le serveur de developpement pour verifier :
pnpm dev --filter=webLe flag --filter=web indique a Turborepo de ne lancer la tache dev que pour l'application web (et ses dependances).
Etape 6 : Ajouter une seconde application (tableau de bord admin)
La vraie puissance des monorepos se revele lorsque plusieurs applications partagent du code.
Creer l'application admin
cd apps
pnpm dlx create-next-app@latest admin --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"Connecter les dependances partagees
Editez apps/admin/package.json pour ajouter les dependances du workspace :
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Mettez a jour apps/admin/next.config.ts :
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@repo/ui", "@repo/utils"],
};
export default nextConfig;Executez install depuis la racine :
cd ..
pnpm installConstruire la page du tableau de bord admin
Creez apps/admin/app/page.tsx :
import { Button } from "@repo/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@repo/ui/card";
import { Badge } from "@repo/ui/badge";
import { formatCurrency, formatDate } from "@repo/utils/format";
const stats = [
{ label: "Total Revenue", value: formatCurrency(48250), change: "+12.5%" },
{ label: "Active Users", value: "2,420", change: "+8.1%" },
{ label: "Orders Today", value: "145", change: "+3.2%" },
{ label: "Conversion Rate", value: "3.6%", change: "-0.4%" },
];
const recentOrders = [
{ id: "ORD-001", customer: "Alice Johnson", amount: 299, status: "completed" as const },
{ id: "ORD-002", customer: "Bob Smith", amount: 149, status: "pending" as const },
{ id: "ORD-003", customer: "Charlie Brown", amount: 499, status: "completed" as const },
{ id: "ORD-004", customer: "Diana Ross", amount: 89, status: "failed" as const },
];
const statusVariant = {
completed: "success",
pending: "warning",
failed: "error",
} as const;
export default function AdminDashboard() {
return (
<main className="mx-auto max-w-6xl p-8">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
<p className="text-gray-500">{formatDate(new Date())}</p>
</div>
<Button variant="primary">Export Report</Button>
</div>
<div className="mb-8 grid gap-4 md:grid-cols-4">
{stats.map((stat) => (
<Card key={stat.label}>
<CardContent>
<p className="text-sm text-gray-500">{stat.label}</p>
<p className="mt-1 text-2xl font-bold">{stat.value}</p>
<Badge variant={stat.change.startsWith("+") ? "success" : "error"}>
{stat.change}
</Badge>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
</CardHeader>
<CardContent>
<table className="w-full">
<thead>
<tr className="border-b text-left text-sm text-gray-500">
<th className="pb-3">Order ID</th>
<th className="pb-3">Customer</th>
<th className="pb-3">Amount</th>
<th className="pb-3">Status</th>
</tr>
</thead>
<tbody>
{recentOrders.map((order) => (
<tr key={order.id} className="border-b last:border-0">
<td className="py-3 font-mono text-sm">{order.id}</td>
<td className="py-3">{order.customer}</td>
<td className="py-3">{formatCurrency(order.amount)}</td>
<td className="py-3">
<Badge variant={statusVariant[order.status]}>{order.status}</Badge>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</main>
);
}Desormais, web et admin partagent les memes composants UI et utilitaires. Modifiez un composant une seule fois, et il se met a jour partout.
Etape 7 : Configurer les pipelines Turborepo
Mettez a jour turbo.json pour gerer toutes les taches efficacement :
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"type-check": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}Ajoutez les nouveaux scripts au package.json racine :
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"dev:web": "turbo run dev --filter=web",
"dev:admin": "turbo run dev --filter=admin",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules .turbo"
}
}Comprendre les dependances entre taches
Le graphe de dependances est la cle de l'efficacite de Turborepo :
build task flow:
@repo/typescript-config (no build needed)
→ @repo/utils#build
→ @repo/ui#build
→ web#build (depends on ui + utils)
→ admin#build (depends on ui + utils)
Turborepo execute utils#build et ui#build en parallele (ils sont independants), puis execute web#build et admin#build en parallele une fois leurs dependances terminees.
Etape 8 : Activer le cache distant
Le cache distant est la fonctionnalite phare de Turborepo. Il permet de partager les artefacts de build entre votre machine locale, vos collegues et le CI — pour que personne ne reconstruise jamais ce que quelqu'un d'autre a deja construit.
Lier votre depot
npx turbo login
npx turbo linkCela connecte votre monorepo au cache distant de Vercel (offre gratuite disponible). Apres la liaison, vous verrez une confirmation :
✓ Linked to my-org/my-monorepo
Comment cela fonctionne
- Premier build : Turborepo execute la tache, hache les entrees (fichiers sources, dependances, variables d'environnement) et televerse la sortie vers le cache distant.
- Second build (ou CI, ou un collegue) : Turborepo calcule le meme hash, le trouve dans le cache distant et telecharge la sortie — en sautant le build reel.
# First build — runs everything
pnpm build
# → web#build: cache miss, computing...
# → admin#build: cache miss, computing...
# Second build — instant replay
pnpm build
# → web#build: cache hit, replaying output
# → admin#build: cache hit, replaying outputPour les grands monorepos, cela peut reduire les temps de CI de 20 minutes a moins d'une minute.
Cache distant auto-heberge
Si vous preferez ne pas utiliser Vercel, vous pouvez auto-heberger le serveur de cache. Turborepo supporte tout stockage compatible S3 :
// turbo.json
{
"remoteCache": {
"enabled": true,
"signature": true
}
}Utilisez les variables d'environnement TURBO_API, TURBO_TOKEN et TURBO_TEAM pour configurer un endpoint auto-heberge.
Etape 9 : Mettre en place le CI/CD avec GitHub Actions
Creez .github/workflows/ci.yml :
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Build
run: pnpm build
- name: Test
run: pnpm testDeployer uniquement les applications modifiees
Turborepo peut detecter quelles applications ont change et ne deployer que celles-ci. Utilisez le flag --filter avec git diff :
deploy-web:
name: Deploy Web
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check for changes
id: changes
run: |
if npx turbo-ignore web; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Deploy web app
if: steps.changes.outputs.skip == 'false'
run: echo "Deploying web app..."
# Add your deployment command hereLa commande turbo-ignore renvoie le code 0 si rien n'a change pour ce package, vous permettant de sauter les deploiements inutiles.
Etape 10 : Patterns avances
Variables d'environnement dans Turborepo
Si la sortie de votre build depend de variables d'environnement, vous devez en informer Turborepo. Sinon, il pourrait servir un build en cache qui utilisait des valeurs d'environnement differentes.
// turbo.json
{
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["DATABASE_URL", "NEXT_PUBLIC_API_URL"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}globalEnv— Variables qui affectent toutes les taches.env— Variables qui affectent une tache specifique. Les modifications de ces variables invalident le cache.
Filtrage des taches
La syntaxe de filtrage de Turborepo est puissante :
# Run build for web and its dependencies
pnpm build --filter=web...
# Run build for everything except docs
pnpm build --filter=!docs
# Run build for packages that changed since main
pnpm build --filter=...[main]
# Run build for web and admin only
pnpm build --filter=web --filter=adminPackages internes vs. publies
Nos packages sont "private": true — ils sont internes, consommes uniquement au sein du monorepo. Pour des packages publies (npm), vous ajouteriez :
{
"name": "@myorg/ui",
"version": "1.0.0",
"private": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts"
}
}Utilisez changesets pour la gestion des versions des packages publies.
Etape 11 : Configuration Tailwind CSS partagee
Lorsque plusieurs applications utilisent Tailwind, partagez la configuration pour maintenir la coherence des design tokens.
Creez packages/tailwind-config/package.json :
{
"name": "@repo/tailwind-config",
"version": "0.0.1",
"private": true,
"main": "./tailwind.config.ts",
"exports": {
".": "./tailwind.config.ts"
},
"devDependencies": {
"tailwindcss": "^4.0.0"
}
}Creez packages/tailwind-config/tailwind.config.ts :
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
950: "#172554",
},
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
mono: ["JetBrains Mono", "monospace"],
},
},
},
plugins: [],
};
export default config;Dans chaque application, etendez la configuration partagee :
// apps/web/tailwind.config.ts
import sharedConfig from "@repo/tailwind-config";
import type { Config } from "tailwindcss";
const config: Config = {
...sharedConfig,
content: [
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
};
export default config;Tester votre monorepo
Verifions que tout fonctionne de bout en bout.
Construire tous les packages
pnpm buildVous devriez voir Turborepo orchestrer le build :
Tasks: 4 successful, 4 total
Cached: 0 cached, 4 total
Time: 12.4s
Relancer le build
pnpm buildCette fois, tout est en cache :
Tasks: 4 successful, 4 total
Cached: 4 cached, 4 total
Time: 0.3s >>> FULL TURBO
Ce message "FULL TURBO" signifie que chaque tache a ete servie depuis le cache — aucun travail effectue.
Lancer les serveurs de developpement
# All apps
pnpm dev
# Just the web app
pnpm dev:webDepannage
Erreurs "Module not found"
Si une application ne trouve pas un package du workspace :
- Verifiez que le package est liste dans les dependances du
package.jsonde l'application - Executez
pnpm installdepuis la racine - Assurez-vous que
transpilePackagesinclut le package dansnext.config.ts
Le cache ne fonctionne pas
Si les builds ne sont pas mis en cache :
- Executez
turbo run build --summarizepour inspecter le hash de la tache - Verifiez que
outputsdansturbo.jsoncorrespond a vos sorties de build reelles - Assurez-vous que les variables d'environnement sont declarees dans
envouglobalEnv
Erreurs de chemins TypeScript
Si votre editeur affiche des erreurs de type pour les packages du workspace :
- Redemarrez le serveur TypeScript (
Cmd+Shift+Ppuis "TypeScript: Restart TS Server") - Assurez-vous que chaque package dispose d'un
tsconfig.jsonqui etend la configuration partagee - Verifiez que
exportsdanspackage.jsonpointe vers les bons fichiers
Resume de la structure du projet
Voici la structure complete du monorepo que vous avez construit :
my-monorepo/
├── apps/
│ ├── web/ # Marketing site
│ │ ├── app/page.tsx # Uses @repo/ui + @repo/utils
│ │ ├── next.config.ts # transpilePackages
│ │ └── package.json # workspace:* deps
│ └── admin/ # Admin dashboard
│ ├── app/page.tsx # Uses @repo/ui + @repo/utils
│ ├── next.config.ts
│ └── package.json
├── packages/
│ ├── ui/ # Shared UI components
│ │ └── src/
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ └── badge.tsx
│ ├── utils/ # Shared utilities
│ │ └── src/
│ │ ├── cn.ts
│ │ ├── format.ts
│ │ └── validators.ts
│ ├── tailwind-config/ # Shared Tailwind theme
│ ├── typescript-config/ # Shared tsconfig
│ └── eslint-config/ # Shared ESLint rules
├── .github/workflows/ci.yml # CI with remote caching
├── turbo.json # Pipeline config
├── pnpm-workspace.yaml
└── package.json
Prochaines etapes
Maintenant que vous disposez d'un monorepo fonctionnel, envisagez :
- Ajouter Storybook au package
uipour la documentation des composants - Configurer Changesets si vous prevoyez de publier des packages sur npm
- Ajouter des tests E2E avec Playwright dans un package
packages/e2esepare - Deployer sur Vercel — qui offre un support de premier ordre pour Turborepo avec detection automatique des projets
- Ajouter un package de base de donnees partage avec Drizzle ORM pour un acces coherent au schema
Conclusion
Vous avez construit un monorepo pret pour la production avec Turborepo qui comprend :
- Deux applications Next.js partageant du code de maniere transparente
- Une bibliotheque UI partagee avec des composants types et reutilisables
- Un package utilitaire partage pour la logique commune
- Des configurations TypeScript et Tailwind partagees
- Des pipelines de build efficaces avec mise en cache et parallelisation
- Des workflows CI/CD avec deploiement selectif
Turborepo transforme la gestion des monorepos d'une corvee en un super-pouvoir. La combinaison du hachage de taches, de l'execution parallele et du cache distant garantit que vos builds restent rapides, quelle que soit la taille de votre codebase. Commencez petit avec un seul package partage, et montez en puissance selon les besoins de votre projet.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Better Auth avec Next.js 15 : Le Guide Complet d'Authentification pour 2026
Apprenez à implémenter une authentification complète dans Next.js 15 avec Better Auth. Ce tutoriel couvre email/mot de passe, OAuth, sessions, protection des routes et contrôle d'accès basé sur les rôles.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.