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 principaleapps/admin— Application Next.js 15 d'administrationlibs/ui— Bibliothèque de composants React partagée (@myorg/ui)libs/utils— Utilitaires TypeScript partagés (@myorg/utils)- Une configuration CI utilisant
nx affectedpour 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 myorgL'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=trueGénérer l'application d'administration :
npx nx generate @nx/next:app admin \
--directory=apps/admin \
--style=css \
--appRouter=trueVérifiez que les deux fonctionnent :
npx nx serve web # tourne sur http://localhost:3000
npx nx serve admin # tourne sur http://localhost:4200Chaque 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/uiL'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/utilsAjoutez 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 graphCela ouvre une interface dans le navigateur montrant :
webdépend deuietutilsadmindépend deuietutilsuin'a aucune dépendanceutilsn'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,lintLe 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=mainScénario concret : Vous corrigez un bug dans libs/utils. Nx calcule :
utils— changé directement ✓ reconstruireweb— dépend deutils✓ reconstruireadmin— dépend deutils✓ reconstruireui— aucune dépendance surutils✗ 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_BASELe 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 connectSuivez 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 \
--exportCela 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/pricingVous 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 graphSi 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/mainLe cache de build est périmé ou corrompu
Videz le cache local et réessayez :
npx nx reset
npx nx build webpnpm é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/apiet créez une bibliothèque@myorg/shared-typespour les types de requêtes/réponses - Intégration Storybook : Lancez
nx generate @nx/storybook:configuration uipour ajouter une vitrine de composants - Module Federation : Utilisez
@nx/module-federationpour 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.