ReactNext.jsTypeScriptArchitectureTurborepo

Design System React en monorepo : Turborepo et packages internes

Publié le

Dimitri Dumont
Dimitri Dumont

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

SignalExemple
2+ applications avec UI similaireApp client + backoffice admin
Code dupliqué entre projetsButton, Modal, Form identiques
Équipe partageant le même cycle de releaseDéploiements coordonnés
Besoin de cohérence UI/UXMême design system sur toutes les apps

Signaux défavorables

SignalAlternative
Une seule applicationDossier /components suffit
Équipes séparées, cycles différentsPackage 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
DossierContenu
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.json dé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.

pnpm-workspace.yaml
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.

package.json
{
  "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.

turbo.json
{
  "$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

OptionRôle
dependsOn^build = build les dépendances avant
outputsDossiers mis en cache pour éviter les rebuilds
cachefalse désactive le cache (utile pour dev)
persistentGarde 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

packages/ui/package.json
{
  "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 npm
  • type: module : ESM natif
  • exports : point d'entrée unique, tous les composants sont ré-exportés depuis index.ts
  • peerDependencies : React fourni par l'app consommatrice
  • workspace:* : référence aux packages internes

TypeScript configuration

packages/ui/tsconfig.json
{
  "extends": "@repo/config-typescript/base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

Exemple de composant

packages/ui/src/components/Button/Button.tsx
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é

packages/ui/src/components/Button/index.ts
export { Button } from "./Button";
packages/ui/src/components/index.ts
export * from "./Button";
packages/ui/src/index.ts
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:*.

apps/web/package.json
{
  "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.

apps/web/next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  transpilePackages: ["@repo/ui"],
};
 
export default nextConfig;

Utilisation dans un composant

apps/web/src/app/page.tsx
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.

packages/config-tailwind/package.json
{
  "name": "@repo/config-tailwind",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./theme.css"
  },
  "devDependencies": {
    "tailwindcss": "^4.0.0"
  }
}
packages/config-tailwind/theme.css
@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

apps/web/src/app/globals.css
@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

.github/workflows/ci.yml
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 test

Déploiement Vercel

Vercel détecte automatiquement les monorepos Turborepo. Configurez chaque app séparément dans le dashboard Vercel.

Pour chaque app :

  1. Root Directory : laisser vide (racine du monorepo)
  2. Build Command : pnpm turbo build --filter=@repo/web
  3. Output Directory : apps/web/.next
  4. 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

  1. Évaluez le besoin avant d'adopter un monorepo (2+ apps, code dupliqué, équipe coordonnée)
  2. Turborepo + pnpm offrent un setup simple et performant
  3. Internal packages (workspace:*) évitent la publication npm
  4. Tailwind v4 partagé via un thème CSS centralisé (@theme) garantit la cohérence
  5. 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/ui avec 5-10 composants de base
  • Créer @repo/config-tailwind avec les tokens
  • Ajouter transpilePackages dans chaque app Next.js
  • Configurer le CI avec cache pnpm

Pour aller plus loin

Dimitri Dumont

À 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 →

Articles similaires