Design System React en monorepo : Turborepo et packages internes
Publié le
Développeur React & Next.js freelance
Partager des composants entre plusieurs applications React ou Next.js génère souvent de la friction : copier-coller de code, divergences d'implémentation, npm link instable. Un monorepo avec packages internes résout ces problèmes en centralisant le code partagé sans publier sur npm.
Cet article présente un setup pragmatique avec Turborepo et pnpm, adapté aux équipes de 2 à 10 développeurs.
Dans cet article :
- Quand adopter (ou éviter) le monorepo
- Structure et configuration Turborepo + pnpm
- Créer un package UI interne
- Partager la configuration Tailwind
- CI/CD et déploiement Vercel
Quand adopter le monorepo
Un monorepo n'est pas toujours la bonne solution. L'overhead de configuration se justifie dans certains contextes.
Signaux favorables
| Signal | Exemple |
|---|---|
| 2+ applications avec UI similaire | App client + backoffice admin |
| Code dupliqué entre projets | Button, Modal, Form identiques |
| Équipe partageant le même cycle de release | Déploiements coordonnés |
| Besoin de cohérence UI/UX | Même design system sur toutes les apps |
Signaux défavorables
| Signal | Alternative |
|---|---|
| Une seule application | Dossier /components suffit |
| Équipes séparées, cycles différents | Package npm publié |
| Stacks différentes (React + Vue) | Package npm publié |
| Moins de 3 développeurs frontend | Évaluer le ROI avant de se lancer |
Si les signaux défavorables dominent, un package npm privé (GitHub Packages, npm privé) sera plus adapté.
Structure du monorepo
La structure recommandée sépare les applications (apps/) des packages partagés (packages/).
my-monorepo/
├── apps/
│ ├── web/
│ └── admin/
├── packages/
│ ├── ui/
│ ├── config-tailwind/
│ └── config-typescript/
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
└── .gitignore
| Dossier | Contenu |
|---|---|
apps/web/ | App Next.js principale |
apps/admin/ | Backoffice Next.js |
packages/ui/ | Design system (composants partagés) |
packages/config-tailwind/ | Thème Tailwind (theme.css) |
packages/config-typescript/ | TSConfig partagé (base.json) |
Cette organisation apporte plusieurs avantages :
- Isolation claire : apps consomment, packages exposent
- Dépendances explicites : chaque
package.jsondéclare ses besoins - Builds parallèles : Turborepo optimise l'exécution
Configuration pnpm workspaces
pnpm gère les dépendances du monorepo via le fichier pnpm-workspace.yaml à la racine.
packages:
- "apps/*"
- "packages/*"Cette configuration indique à pnpm que tous les dossiers dans apps/ et packages/ sont des packages du workspace.
Package.json racine
Le package.json racine définit les scripts globaux et les devDependencies partagées.
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"lint": "turbo lint",
"test": "turbo test",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.7.0",
"typescript": "^5.7.0"
},
"packageManager": "pnpm@10.0.0"
}Le champ packageManager garantit que tous les contributeurs utilisent la même version de pnpm.
Configuration Turborepo
Turborepo orchestre les builds et optimise l'exécution via le cache.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"clean": {
"cache": false
}
}
}Explication des options
| Option | Rôle |
|---|---|
dependsOn | ^build = build les dépendances avant |
outputs | Dossiers mis en cache pour éviter les rebuilds |
cache | false désactive le cache (utile pour dev) |
persistent | Garde le processus actif (mode watch) |
Créer le package UI
Le package @repo/ui contient les composants partagés du design system.
Configuration du package
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"scripts": {
"lint": "eslint src/",
"test": "vitest"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.7.0"
}
}Points clés
private: true: pas de publication npmtype: module: ESM natifexports: point d'entrée unique, tous les composants sont ré-exportés depuisindex.tspeerDependencies: React fourni par l'app consommatriceworkspace:*: référence aux packages internes
TypeScript configuration
{
"extends": "@repo/config-typescript/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}Exemple de composant
import { type ButtonHTMLAttributes, forwardRef } from "react";
type ButtonVariant = "primary" | "secondary" | "danger";
type ButtonSize = "sm" | "md" | "lg";
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
const variantStyles: Record<ButtonVariant, string> = {
primary: "bg-primary-600 text-white hover:bg-primary-700",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
danger: "bg-red-600 text-white hover:bg-red-700",
};
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",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", size = "md", className = "", ...props }, ref) => {
const baseStyles = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none";
return (
<button
ref={ref}
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
{...props}
/>
);
}
);
Button.displayName = "Button";Export centralisé
export { Button } from "./Button";export * from "./Button";export * from "./components";Consommer le package dans une app
Pour utiliser @repo/ui dans une application Next.js, ajoutez la dépendance avec le protocole workspace:*.
{
"name": "@repo/web",
"dependencies": {
"@repo/ui": "workspace:*",
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Configuration Next.js
Next.js doit transpiler les packages internes. Ajoutez transpilePackages dans la configuration.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@repo/ui"],
};
export default nextConfig;Utilisation dans un composant
import { Button } from "@repo/ui";
export default function HomePage() {
return (
<main>
<h1>Application Web</h1>
<Button variant="primary" size="lg">
Action principale
</Button>
</main>
);
}Partager la configuration Tailwind
Centraliser la configuration Tailwind évite la duplication des tokens (couleurs, espacements, typographie).
Package config-tailwind
Tailwind CSS v4 utilise une approche CSS-first avec la directive @theme. Le package partagé contient un fichier CSS avec les design tokens.
{
"name": "@repo/config-tailwind",
"version": "0.0.0",
"private": true,
"exports": {
".": "./theme.css"
},
"devDependencies": {
"tailwindcss": "^4.0.0"
}
}@theme {
--color-primary-50: #fef7ed;
--color-primary-100: #fdecd4;
--color-primary-200: #fad5a8;
--color-primary-300: #f6b871;
--color-primary-400: #f19038;
--color-primary-500: #ee7413;
--color-primary-600: #df5a09;
--color-primary-700: #b9420a;
--color-primary-800: #933510;
--color-primary-900: #772d10;
--font-sans: "Geist Sans", system-ui, sans-serif;
}La directive @theme crée automatiquement les classes utilitaires correspondantes (bg-primary-500, text-primary-600, font-sans).
Consommer le thème dans une app
@import "tailwindcss";
@import "@repo/config-tailwind";
@source "../../../../packages/ui/src";La directive @source indique à Tailwind de scanner les fichiers du package UI pour détecter les classes utilisées. Le chemin est relatif au fichier CSS (apps/web/src/app/globals.css), d'où les 4 niveaux de remontée vers la racine du monorepo.
CI/CD et déploiement
GitHub Actions avec cache Turborepo
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Lint
run: pnpm lint
- name: Test
run: pnpm testDéploiement Vercel
Vercel détecte automatiquement les monorepos Turborepo. Configurez chaque app séparément dans le dashboard Vercel.
Pour chaque app :
- Root Directory : laisser vide (racine du monorepo)
- Build Command :
pnpm turbo build --filter=@repo/web - Output Directory :
apps/web/.next - Install Command :
pnpm install
Vercel utilise le cache Turborepo distant pour accélérer les builds. L'option --filter garantit que seule l'app ciblée et ses dépendances sont buildées.
Les erreurs à éviter
Bundler les composants UI
Compiler le package UI en JavaScript (avec tsup, Rollup) ajoute de la complexité. Next.js transpile directement le TypeScript via transpilePackages. Évitez le build intermédiaire sauf si vous publiez sur npm.
Oublier transpilePackages
Sans cette option, Next.js ne compile pas les packages internes et génère des erreurs de syntaxe.
// ❌ Erreur fréquente : import non transpilé
Module parse failed: Unexpected token
// ✅ Solution : ajouter transpilePackages
transpilePackages: ["@repo/ui"]Circular dependencies
Si le package A dépend de B qui dépend de A, Turborepo ne peut pas résoudre l'ordre de build. Restructurez pour éliminer les cycles.
Trop de packages trop tôt
Commencez avec 2-3 packages (ui, config-tailwind, config-typescript). Ajoutez-en uniquement quand le besoin se confirme.
FAQ
Pourquoi Turborepo et pnpm ?
Turborepo est le choix pragmatique pour les projets Next.js : configuration minimale, intégration native Vercel, performances excellentes grâce au cache intelligent. Nx offre plus de fonctionnalités (générateurs, graph visuel) mais avec plus de complexité. Lerna est en maintenance, préférez les alternatives modernes.
pnpm plutôt que npm ou yarn pour plusieurs raisons : installation plus rapide grâce au stockage global des dépendances, économie d'espace disque via les liens symboliques, et gestion native des workspaces avec un fichier pnpm-workspace.yaml explicite. Le champ packageManager dans le package.json racine garantit que toute l'équipe utilise la même version.
Faut-il Storybook dès le début ?
Storybook est un outil utile pour les développeurs : il facilite la compréhension et l'utilisation des composants, permet de les tester visuellement en isolation. Cependant, il ajoute de la maintenance. Pour un projet qui démarre, une page /docs dans l'app principale ou un fichier README peut suffire. Storybook devient pertinent quand l'équipe grandit (5+ développeurs) ou quand les designers ont besoin d'une référence visuelle partagée.
Comment gérer les assets (icônes, fonts) ?
Centralisez les assets dans un package @repo/assets ou dans @repo/ui/assets. Exportez-les via le package.json. Pour les fonts, préférez next/font dans chaque app plutôt qu'un package partagé.
Peut-on migrer progressivement vers un monorepo ?
Oui. Commencez par créer la structure monorepo vide, puis migrez une app. Extrayez ensuite les composants partagés dans @repo/ui. La migration incrémentale limite les risques.
Quel impact sur les temps de build ?
Le cache Turborepo réduit significativement les temps de build. Après le premier build, seuls les packages modifiés sont reconstruits. Avec le cache distant (Vercel, Turborepo Cloud), les gains sont partagés entre développeurs et CI.
Conclusion
Points clés
- Évaluez le besoin avant d'adopter un monorepo (2+ apps, code dupliqué, équipe coordonnée)
- Turborepo + pnpm offrent un setup simple et performant
- Internal packages (
workspace:*) évitent la publication npm - Tailwind v4 partagé via un thème CSS centralisé (
@theme) garantit la cohérence - transpilePackages est obligatoire pour les packages TypeScript internes
Checklist de démarrage
- Créer la structure
apps/+packages/ - Configurer
pnpm-workspace.yaml - Configurer
turbo.json - Créer
@repo/uiavec 5-10 composants de base - Créer
@repo/config-tailwindavec les tokens - Ajouter
transpilePackagesdans chaque app Next.js - Configurer le CI avec cache pnpm
Pour aller plus loin

À propos de l'auteur
Je suis Dimitri Dumont, développeur freelance spécialisé React & Next.js depuis plus de 7 ans. J'ai accompagné 22 startups et réalisé 43 missions avec une note de 5/5. J'applique ces patterns au quotidien pour continuer de livrer rapidement du code évolutif. En savoir plus →
Cet article vous a été utile ?
Je peux vous accompagner sur votre projet React & Next.js.
Discutons de votre projet →