écrits/tutorial/2026/07
Tutorial29 juil. 2026·30 min

Nx Monorepo : Mise à l'échelle des projets Next.js et TypeScript avec des bibliothèques partagées

Apprenez à construire un Nx monorepo avec plusieurs applications Next.js et des bibliothèques TypeScript partagées. Ce guide couvre la configuration de l'espace de travail, la génération de bibliothèques, le graphe de dépendances et Nx Affected pour des pipelines CI considérablement plus rapides.

Introduction

Lorsque votre produit passe d'une application unique à plusieurs projets liés — un site client, un panneau d'administration, une bibliothèque de composants partagés — maintenir la synchronisation du code entre des dépôts séparés devient pénible. Vous finissez par copier-coller des composants, maintenir de la logique dupliquée et redouter le moment où un utilitaire partagé doit être mis à jour en cinq endroits simultanément.

Nx est un système de build intelligent conçu exactement pour ce problème. Il transforme plusieurs projets en un seul espace de travail cohérent où :

  • Les bibliothèques partagées s'importent comme n'importe quel paquet npm (@myorg/ui)
  • Seul le code affecté par vos changements est reconstruit ou retesté
  • Les générateurs imposent un scaffolding cohérent dans toutes les équipes
  • Le cache distant via Nx Cloud fait tourner le CI en secondes, pas en minutes

Ce guide vous accompagne pas à pas dans la construction d'un espace de travail Nx prêt pour la production, avec deux applications Next.js partageant une bibliothèque de composants UI et une bibliothèque d'utilitaires.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20 ou supérieur (node --version)
  • pnpm 8 ou supérieur (npm install -g pnpm)
  • Des bases en TypeScript et React/Next.js
  • Git configuré sur votre machine

Ce que vous allez construire

À la fin de ce tutoriel, votre espace de travail contiendra :

  • apps/web — Application Next.js 15 principale
  • apps/admin — Application Next.js 15 d'administration
  • libs/ui — Bibliothèque de composants React partagée (@myorg/ui)
  • libs/utils — Utilitaires TypeScript partagés (@myorg/utils)
  • Une configuration CI utilisant nx affected pour ignorer les projets non modifiés

Étape 1 : Créer l'espace de travail Nx

Utilisez la commande de scaffolding officielle pour démarrer un espace de travail vide :

npx create-nx-workspace@latest myorg --preset=empty --pm=pnpm
cd myorg

L'option --preset=empty crée un espace de travail minimal — aucune application pour l'instant, juste la configuration Nx. Vous trouverez :

myorg/
├── nx.json              # Configuration Nx et règles de mise en cache
├── package.json         # Dépendances racine
├── pnpm-workspace.yaml  # Configuration pnpm workspace
└── tsconfig.base.json   # Chemins TypeScript partagés

Installez les plugins nécessaires :

pnpm add -D @nx/next @nx/react @nx/js

Étape 2 : Ajouter deux applications Next.js

Générer l'application web principale :

npx nx generate @nx/next:app web \
  --directory=apps/web \
  --style=css \
  --appRouter=true

Générer l'application d'administration :

npx nx generate @nx/next:app admin \
  --directory=apps/admin \
  --style=css \
  --appRouter=true

Vérifiez que les deux fonctionnent :

npx nx serve web    # tourne sur http://localhost:3000
npx nx serve admin  # tourne sur http://localhost:4200

Chaque application est totalement indépendante — next.config.js séparé, routeur app/ séparé, port différent.

Étape 3 : Créer une bibliothèque UI partagée

Générez une bibliothèque de composants React que les deux applications peuvent importer :

npx nx generate @nx/react:library ui \
  --directory=libs/ui \
  --unitTestRunner=vitest \
  --bundler=vite \
  --importPath=@myorg/ui

L'option --importPath définit l'alias d'import. N'importe quelle application dans l'espace de travail peut désormais écrire import { Button } from '@myorg/ui' et TypeScript le résout automatiquement.

Créez un composant Button réutilisable :

// libs/ui/src/lib/button/button.tsx
import React from 'react';
 
export interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger';
  onClick?: () => void;
  disabled?: boolean;
  className?: string;
}
 
export function Button({
  children,
  variant = 'primary',
  onClick,
  disabled,
  className = '',
}: ButtonProps) {
  const base = 'px-4 py-2 rounded font-medium transition-colors focus:outline-none focus:ring-2';
  const variants = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200 focus:ring-gray-400',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
  };
 
  return (
    <button
      className={`${base} ${variants[variant]} ${className}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

Créez un composant Card :

// libs/ui/src/lib/card/card.tsx
import React from 'react';
 
export interface CardProps {
  title: string;
  description?: string;
  children?: React.ReactNode;
  className?: string;
}
 
export function Card({ title, description, children, className = '' }: CardProps) {
  return (
    <div className={`rounded-lg border border-gray-200 bg-white p-6 shadow-sm ${className}`}>
      <h3 className="mb-1 text-lg font-semibold text-gray-900">{title}</h3>
      {description && <p className="mb-4 text-sm text-gray-600">{description}</p>}
      {children}
    </div>
  );
}

Exportez tout depuis le point d'entrée de la bibliothèque :

// libs/ui/src/index.ts
export * from './lib/button/button';
export * from './lib/card/card';

Étape 4 : Créer une bibliothèque d'utilitaires partagée

Générez une bibliothèque TypeScript pure (sans dépendance React) pour la logique métier :

npx nx generate @nx/js:library utils \
  --directory=libs/utils \
  --unitTestRunner=vitest \
  --bundler=tsc \
  --importPath=@myorg/utils

Ajoutez des helpers de formatage :

// libs/utils/src/lib/format.ts
export function formatDate(date: Date, locale = 'fr-FR'): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date);
}
 
export function formatCurrency(
  amount: number,
  currency = 'EUR',
  locale = 'fr-FR'
): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).format(amount);
}
 
export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}
 
export function truncate(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength).trimEnd() + '…';
}

Ajoutez les types TypeScript partagés :

// libs/utils/src/lib/types.ts
export interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}
 
export interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}
 
export interface SelectOption<T = string> {
  label: string;
  value: T;
  disabled?: boolean;
}

Exportez depuis le point d'entrée :

// libs/utils/src/index.ts
export * from './lib/format';
export * from './lib/types';

Étape 5 : Utiliser les bibliothèques partagées dans vos applications

Importez les bibliothèques partagées dans vos applications Next.js exactement comme des paquets npm :

// apps/web/app/page.tsx
import { Button, Card } from '@myorg/ui';
import { formatDate, formatCurrency } from '@myorg/utils';
 
export default function HomePage() {
  const today = new Date();
  const price = 149.99;
 
  return (
    <main className="mx-auto max-w-4xl p-8">
      <h1 className="mb-6 text-4xl font-bold">Ma Boutique</h1>
 
      <div className="mb-8 grid gap-4 sm:grid-cols-2">
        <Card title="Date du jour" description={formatDate(today)}>
          <Button variant="secondary">Voir le calendrier</Button>
        </Card>
 
        <Card title="Produit vedette" description={`Prix : ${formatCurrency(price)}`}>
          <Button variant="primary">Ajouter au panier</Button>
        </Card>
      </div>
    </main>
  );
}

L'application admin utilise les mêmes composants :

// apps/admin/app/page.tsx
import { Button, Card } from '@myorg/ui';
import { formatDate } from '@myorg/utils';
 
export default function AdminDashboard() {
  return (
    <main className="p-8">
      <h1 className="mb-4 text-2xl font-bold">Tableau de bord Admin</h1>
      <p className="mb-6 text-gray-600">
        Dernière synchronisation : {formatDate(new Date())}
      </p>
 
      <div className="flex gap-3">
        <Button variant="primary">Exporter les données</Button>
        <Button variant="danger">Vider le cache</Button>
      </div>
    </main>
  );
}

TypeScript résout les imports via les alias de chemins dans tsconfig.base.json que Nx a configurés automatiquement.

Étape 6 : Explorer le graphe de projets

Nx construit un graphe de dépendances en temps réel de votre espace de travail. Visualisez-le avec :

npx nx graph

Cela ouvre une interface dans le navigateur montrant :

  • web dépend de ui et utils
  • admin dépend de ui et utils
  • ui n'a aucune dépendance
  • utils n'a aucune dépendance

Ce graphe permet à Nx de savoir ce qu'il faut reconstruire quand des fichiers changent. Si vous modifiez libs/utils/src/lib/format.ts, Nx sait que web et admin sont impactés et doivent être retestés.

Étape 7 : Exécuter des tâches dans tout l'espace de travail

Nx fournit une interface unifiée pour exécuter n'importe quelle tâche dans n'importe quel projet :

# Exécuter un projet unique
npx nx build web
npx nx test ui
npx nx lint utils
 
# Exécuter une tâche sur tous les projets
npx nx run-many -t build
npx nx run-many -t test
npx nx run-many -t lint
 
# Exécuter plusieurs tâches en parallèle
npx nx run-many -t build,test,lint

Le cache local rend les builds répétés instantanés :

> nx build web

✔  nx run utils:build  (231ms)
✔  nx run ui:build     (1.2s)
✔  nx run web:build    (4.1s)

> nx build web  [deuxième exécution]

✔  nx run utils:build  [cache local]  (0s)
✔  nx run ui:build     [cache local]  (0s)
✔  nx run web:build    [cache local]  (0s)

Étape 8 : Nx Affected — La révolution CI

C'est la fonctionnalité la plus puissante de Nx. Au lieu de reconstruire et retester tout à chaque commit, nx affected ne traite que les projets impactés par vos changements.

# Voir ce qui a changé vs main
npx nx affected -t build --base=main --head=HEAD
 
# Tester uniquement les projets affectés
npx nx affected -t test --base=main
 
# lint, test et build des projets affectés
npx nx affected -t lint,test,build --base=main

Scénario concret : Vous corrigez un bug dans libs/utils. Nx calcule :

  • utils — changé directement ✓ reconstruire
  • web — dépend de utils ✓ reconstruire
  • admin — dépend de utils ✓ reconstruire
  • ui — aucune dépendance sur utils ✗ ignorer

Si vous aviez 10 applications, seules celles dépendant de utils seraient traitées.

Configuration GitHub Actions

# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Historique complet requis pour nx affected
 
      - uses: pnpm/action-setup@v4
        with:
          version: 8
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
 
      - run: pnpm install --frozen-lockfile
 
      - name: Définir NX_BASE pour PR
        if: github.event_name == 'pull_request'
        run: echo "NX_BASE=origin/${{ github.base_ref }}" >> $GITHUB_ENV
 
      - name: Définir NX_BASE pour push
        if: github.event_name == 'push'
        run: echo "NX_BASE=HEAD~1" >> $GITHUB_ENV
 
      - run: npx nx affected -t lint,test,build --base=$NX_BASE

Le fetch-depth: 0 est indispensable — Nx a besoin de l'historique git complet pour comparer les branches.

Étape 9 : Nx Cloud pour le cache distant

Le cache local vous aide, mais Nx Cloud partage le cache avec toute votre équipe et le CI :

npx nx connect

Suivez les instructions pour connecter votre espace de travail. Une fois connecté :

  • Un collègue exécute nx build web → résultat mis en cache dans Nx Cloud
  • Le CI exécute le même build → hit de cache instantané, zéro calcul
  • Vous tirez les derniers changements et exécutez → hit de cache instantané

Le plan gratuit couvre les projets open-source et les petites équipes. Vérifiez nx.json après la connexion :

{
  "nxCloudAccessToken": "YOUR_TOKEN_HERE",
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx-cloud",
      "options": {
        "cacheableOperations": ["build", "test", "lint", "e2e"]
      }
    }
  }
}

Étape 10 : Générateurs pour un scaffolding cohérent

Les générateurs Nx sont comme des templates de code que votre équipe exécute plutôt que de créer des fichiers manuellement.

Générer un nouveau composant dans la bibliothèque UI :

npx nx generate @nx/react:component badge \
  --project=ui \
  --directory=src/lib/badge \
  --export

Cela crée badge.tsx, badge.spec.tsx et met à jour index.ts automatiquement.

Générer une nouvelle page dans une application :

npx nx generate @nx/next:page pricing \
  --project=web \
  --directory=app/pricing

Vous pouvez aussi écrire des générateurs personnalisés pour les patterns de votre équipe — par exemple, un générateur qui crée une page CRUD complète avec les types TypeScript, la route API et les tests.

Tester votre implémentation

Suivez cette liste de vérification pour confirmer que votre espace de travail fonctionne correctement :

# 1. Installer toutes les dépendances
pnpm install
 
# 2. Construire les bibliothèques partagées
npx nx build ui
npx nx build utils
 
# 3. Lancer l'app web — doit afficher Button et Card partagés
npx nx serve web
# Ouvrir http://localhost:3000
 
# 4. Lancer l'app admin
npx nx serve admin
# Ouvrir http://localhost:4200
 
# 5. Exécuter tous les tests
npx nx run-many -t test
 
# 6. Vérifier que TypeScript est content
npx nx run-many -t typecheck
 
# 7. Visualiser le graphe de dépendances
npx nx graph

Si l'application web se charge et affiche les composants Button et Card avec la date et le prix formatés, tout fonctionne.

Résolution des problèmes

TypeScript ne trouve pas @myorg/ui ou @myorg/utils

Ouvrez tsconfig.base.json à la racine et vérifiez la présence des alias de chemins :

{
  "compilerOptions": {
    "paths": {
      "@myorg/ui": ["libs/ui/src/index.ts"],
      "@myorg/utils": ["libs/utils/src/index.ts"]
    }
  }
}

S'ils manquent, relancez le générateur de bibliothèque — il devrait les ajouter automatiquement.

nx affected marque tout comme affecté

Cela arrive quand Nx ne peut pas trouver le commit de base. Assurez-vous que votre checkout CI utilise fetch-depth: 0. En local :

git fetch origin main
npx nx affected -t build --base=origin/main

Le cache de build est périmé ou corrompu

Videz le cache local et réessayez :

npx nx reset
npx nx build web

pnpm échoue avec des erreurs de peer dependency

Ajoutez à package.json :

{
  "pnpm": {
    "peerDependencyRules": {
      "allowedVersions": {
        "react": "19"
      }
    }
  }
}

Prochaines étapes

Maintenant que votre monorepo fonctionne, envisagez ces extensions :

  • Ajouter une couche backend : Générez une app Hono ou NestJS dans apps/api et créez une bibliothèque @myorg/shared-types pour les types de requêtes/réponses
  • Intégration Storybook : Lancez nx generate @nx/storybook:configuration ui pour ajouter une vitrine de composants
  • Module Federation : Utilisez @nx/module-federation pour diviser les grandes apps en micro-frontends indépendants partageant du code à l'exécution
  • Tests E2E Playwright : Ajoutez nx generate @nx/playwright:configuration --project=web-e2e
  • Explorez les plugins Nx : Migrations de base de données, builds Docker et déploiement s'intègrent via des plugins communautaires

Conclusion

Vous avez construit un Nx monorepo prêt pour la production où deux applications Next.js partagent des composants UI et des fonctions utilitaires sans duplication. Les points essentiels à retenir :

  • Les bibliothèques partagées rendent les changements atomiques — corrigez une fois, bénéficiez partout
  • Le graphe de projets rend toutes les dépendances explicites et interrogeables
  • Nx Affected élimine le temps CI gaspillé quand la base de code grandit — seul ce qui a changé est traité
  • Les générateurs imposent des patterns cohérents pour que chaque développeur suive les mêmes conventions
  • Nx Cloud étend le cache du local à toute l'équipe

Les monorepos brillent particulièrement quand les équipes grandissent : plutôt que de coordonner des changements cassants sur plusieurs dépôts et publications npm, un seul commit met tout à jour de façon atomique. Nx rend cette coordination invisible.