
Monorepo et design system : retour d'expérience
Quand une équipe maintient plusieurs applications (un site public, un back-office, une app mobile…), les mêmes composants finissent par être copiés-collés d'un projet à l'autre. Un bouton corrigé ici reste bogué là-bas, les couleurs divergent, et chaque mise à jour devient une chasse au trésor.
Sur un projet de plateforme pour courtiers, nous avons résolu ce problème en regroupant les applications et un design system commun dans un monorepo. Voici ce que nous avons appris.
Monorepo : de quoi parle-t-on ?
Un monorepo est un dépôt Git unique qui contient plusieurs projets (applications et librairies), chacun avec son propre package.json. Ce n'est pas un monolithe : les projets restent indépendants, mais ils partagent l'outillage, l'historique et peuvent dépendre les uns des autres sans passer par un registre npm.
Une structure qui a fait ses preuves
.
├── apps/
│ ├── web/ # site public (Next.js)
│ ├── admin/ # back-office
│ └── mobile/ # React Native
├── packages/
│ ├── ui/ # design system : composants + tokens
│ ├── config/ # ESLint, TypeScript, Tailwind partagés
│ └── api-client/ # client typé de l'API
├── pnpm-workspace.yaml
└── turbo.jsonLa règle : les `apps` dépendent des `packages`, jamais l'inverse, et les packages ne dépendent pas des apps. Cela garde un graphe de dépendances simple et évite les cycles.
Les outils
- Les workspaces du gestionnaire de paquets (pnpm, npm ou Yarn) : ils lient les packages locaux entre eux. Avec pnpm, on déclare
"@acme/ui": "workspace:*"dans l'app. - Un orchestrateur de tâches : Turborepo ou Nx exécutent
build,lintettestdans le bon ordre, en parallèle, et mettent en cache les résultats. Si le packageuin'a pas changé, son build n'est pas relancé. - Lerna, historiquement l'outil de référence, est aujourd'hui maintenu par l'équipe de Nx et reste pertinent pour la publication et le versionnement de packages. Changesets est une alternative légère très répandue.
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"lint": {},
"test": { "dependsOn": ["^build"] }
}
}Le ^build signifie « construire d'abord les dépendances » : le design system est compilé avant les applications qui l'utilisent.
Construire le design system
- Commencer par les tokens (couleurs, espacements, typographie, rayons) exposés en variables CSS. Ils permettent de changer de thème sans toucher aux composants.
- Des composants accessibles par défaut : s'appuyer sur des primitives éprouvées (Radix UI, React Aria) plutôt que de réécrire la gestion du focus et du clavier.
- Documenter avec Storybook : chaque composant a ses variantes visibles, ce qui sert de référence commune aux designers et aux développeurs.
- Exporter proprement : un point d'entrée par composant (
@acme/ui/button) facilite le tree-shaking.
Les pièges que nous avons rencontrés
- Des versions de React différentes entre les apps : un seul exemplaire de React doit être résolu, sinon les hooks cassent. On le déclare en
peerDependenciesdans le design system. - Une CI qui reconstruit tout à chaque commit : sans cache distant ni filtrage des projets impactés (
turbo run build --filter=...[origin/main]), les temps de build explosent. - Un design system qui devient un fourre-tout : un composant métier propre à une seule app n'a rien à y faire. Il doit rester générique.
Ce que ça nous a apporté
Le principal gain a été le time-to-market : une nouvelle application démarrait avec l'authentification, le thème et une vingtaine de composants prêts à l'emploi. Une correction dans le design system se propageait à toutes les apps en une seule pull request, testée de bout en bout.
Conclusion
Le monorepo n'est pas une solution miracle : il demande un outillage sérieux et de la discipline sur les dépendances. Mais dès qu'une équipe maintient plusieurs applications qui partagent une identité visuelle et du code métier, c'est l'un des investissements les plus rentables qu'on puisse faire.