ITSkillsCenter
Blog

Initialiser un monorepo Nx 22 avec NestJS 11

12 دقائق للقراءة

Un monorepo Nx 22 est aujourd’hui le squelette le plus efficace pour un produit qui démarre avec une API NestJS et qui prévoit d’ajouter rapidement un client web ou mobile. Plutôt que de gérer trois dépôts git, trois pipelines CI et trois sets de dépendances qui dérivent dans le temps, on partage un seul workspace, des libs de types communes, et une commande nx affected qui ne reconstruit que ce qui a changé. Ce tutoriel pose les briques d’un monorepo prêt pour la production : workspace Nx 22.6, application NestJS 11.1, libs de domaine et de types, configuration ESLint et TypeScript stricte, et une CI GitHub Actions qui exploite la détection des projets affectés.

📍 Article principal : NestJS 11 pour startup : architecture production 2026. Le monorepo décrit ici est la fondation sur laquelle se branchent les briques Prisma, GraphQL, BullMQ, S3 et Coolify abordées dans les autres tutoriels.

Prérequis

  • Node.js 22.x LTS (codename Jod) — vérifier avec node -v
  • pnpm 9.x — gestionnaire de paquets recommandé pour les monorepos (gain de 40 % sur l’install vs npm)
  • Git 2.40+ pour profiter de git diff --name-only rapide sur gros monorepo
  • Un compte GitHub avec un dépôt vide pour la partie CI
  • Niveau attendu : intermédiaire — connaissance de base de NestJS et de TypeScript
  • Temps estimé : 60 à 90 minutes

Étape 1 — Créer le workspace Nx

Le générateur officiel create-nx-workspace propose plusieurs presets. Pour un produit avec une API backend et des libs partagées, le preset le plus pertinent est nest qui crée d’emblée une application NestJS et configure les tsconfig pour TypeScript strict. On peut aussi partir d’un workspace vide et ajouter les plugins à la demande, ce qui est l’approche choisie ici parce qu’elle reflète mieux ce qu’on veut comprendre.

pnpm dlx create-nx-workspace@22.6.5 acme \
  --preset=apps \
  --packageManager=pnpm \
  --nxCloud=skip
cd acme

L’option --preset=apps crée un workspace minimal sans framework imposé. --nxCloud=skip désactive l’invitation à activer Nx Cloud (gratuit pour les projets open source mais payant au-delà). Une fois la commande terminée, le dossier acme/ contient un nx.json, un package.json racine et des dossiers apps/ et libs/ vides. Le résultat se vérifie immédiatement : nx graph ouvre une interface web qui dessine le graphe des projets — pour l’instant vide, ce qui est attendu.

Étape 2 — Ajouter le plugin Nest et générer l’application api

Le plugin @nx/nest apporte les générateurs spécifiques NestJS : création d’app, génération de modules, controllers, services, et exécuteurs pour nx serve et nx test. Sa version doit toujours correspondre à la version de Nx présente dans package.json sous peine d’erreurs de migration au prochain nx migrate.

pnpm add -D @nx/nest@22.6.5
nx g @nx/nest:application api \
  --linter=eslint \
  --unitTestRunner=jest \
  --e2eTestRunner=jest

Le générateur crée apps/api/ avec la structure NestJS standard (main.ts, app.module.ts, app.controller.ts, app.service.ts) et un projet apps/api-e2e/ pour les tests d’intégration. La commande nx serve api démarre le serveur sur le port 3000 et affiche l’URL dans le terminal. Pour vérifier que tout fonctionne, un curl http://localhost:3000/api doit renvoyer {"message":"Hello API"}. Si le port 3000 est déjà occupé, exporter PORT=3001 avant la commande.

Étape 3 — Créer une bibliothèque de types partagée

L’intérêt principal d’un monorepo est de partager du code entre plusieurs applications. Une bibliothèque shared-types qui contient les DTO et les enums consommés par le backend et par les futurs clients évite que la même interface soit dupliquée dans deux dépôts et qu’elle dérive avec le temps. Le générateur @nx/js crée une lib TypeScript pure, sans framework, parfaite pour ce rôle.

pnpm add -D @nx/js@22.6.5
nx g @nx/js:library shared-types \
  --directory=libs/shared-types \
  --bundler=tsc \
  --unitTestRunner=jest

La lib se trouve dans libs/shared-types/src/ avec un index.ts qui exporte ce que les autres projets pourront consommer. Pour la rendre utilisable, ajouter une interface dans libs/shared-types/src/lib/user.dto.ts et la ré-exporter depuis index.ts. Côté apps/api, l’import se fait avec import { UserDto } from '@acme/shared-types' grâce à la résolution de chemins déjà configurée par Nx dans tsconfig.base.json. Cette résolution remplace les imports relatifs fragiles par des alias stables qui survivent aux refactorings.

Étape 4 — Activer TypeScript strict et ESLint partagé

Un projet qui démarre sans "strict": true dans tsconfig.base.json accumule des dettes de typage qui coûtent cher à rembourser plus tard. Activer le mode strict dès le premier commit force à écrire du code défensif qui révèle les bugs au build plutôt qu’en production. ESLint partagé par tous les projets garantit que le style reste cohérent quand l’équipe s’agrandit.

// tsconfig.base.json (extrait)
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

L’option noUncheckedIndexedAccess est probablement la plus utile pour un backend : elle force à vérifier qu’un accès à un tableau ou à un objet retourne potentiellement undefined. Elle élimine d’un coup une famille entière de bugs cannot read property of undefined en production. Pour ESLint, nx lint api utilise la configuration générée et l’étendre avec des règles équipe se fait via le fichier .eslintrc.json à la racine du workspace.

Étape 5 — Découper l’API en modules métier

NestJS encourage le découpage par domaine plutôt que par couche technique. Plutôt qu’un dossier controllers/, un dossier services/ et un dossier dtos/ qui mélangent toutes les fonctionnalités, on crée un dossier par module métier. Cette discipline rend le code lisible quand l’API atteint cinquante endpoints et permet de générer des libs Nx dédiées par domaine si une feature devient complexe.

nx g @nx/nest:module billing --project=api
nx g @nx/nest:controller billing --project=api --module=billing
nx g @nx/nest:service billing --project=api --module=billing

Les trois commandes créent apps/api/src/billing/ avec billing.module.ts, billing.controller.ts et billing.service.ts, et enregistrent automatiquement le module dans app.module.ts. Le module BillingModule n’expose à l’extérieur que ce qu’il déclare dans exports — par défaut rien, ce qui force à réfléchir au contrat avant d’autoriser un autre module à utiliser le service. Cette frontière explicite est ce qui distingue une architecture saine d’une soupe de services.

Étape 6 — Configurer la CI GitHub Actions avec nx affected

Le vrai gain d’un monorepo Nx en CI vient de la commande nx affected, qui calcule les projets impactés par un commit en analysant le graphe des dépendances. Une pull request qui ne touche que libs/shared-types ne déclenche pas la suite e2e du backend si aucun consommateur n’est affecté. Sur un projet réel, ça divise par trois ou quatre le temps total de la CI.

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
    branches: [main]
jobs:
  affected:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-node@v5
        with: { node-version: 22 }
      - uses: pnpm/action-setup@v6
        with: { version: 9 }
      - run: pnpm install --frozen-lockfile
      - run: npx nx affected -t lint test build --base=origin/main

Le fetch-depth: 0 est obligatoire : sans l’historique git complet, Nx ne peut pas calculer la base de comparaison et il finit par tout reconstruire. Le flag --base=origin/main indique la branche de référence. La première exécution sur un workspace de cinq projets prend trois à quatre minutes ; les suivantes, qui ne touchent qu’un projet, descendent souvent sous la minute. Pour aller plus loin, le tutoriel CI parallélisée avec GitHub Actions détaille la matrice de jobs et le partage de cache.

Étape 7 — Vérifier le graphe et figer les dépendances

Avant de pousser le premier commit, deux vérifications valent leur poids en or. La première est nx graph qui ouvre une interface visuelle du graphe de dépendances. On y détecte immédiatement les imports circulaires ou les libs qui dépendent d’apps (anti-pattern : une lib doit pouvoir être consommée par n’importe quelle app, donc ne dépend jamais d’une app). La seconde est nx report qui liste les versions des plugins Nx installés et signale les écarts.

nx graph                  # interface web sur localhost:4211
nx report                 # versions des plugins
nx affected:graph         # graphe des affectés depuis main

Si nx report remonte un plugin en version différente du noyau Nx, la commande nx migrate latest aligne tout le monde et applique automatiquement les codemods nécessaires. C’est l’opération à faire tous les trois mois pour rester sur les patches de sécurité sans accumuler de dette.

Étape 8 — Tirer parti du cache local Nx

Nx met en cache la sortie des tâches build, test et lint dans .nx/cache/. Une seconde exécution sur le même commit, même après reboot de la machine, restitue instantanément la sortie sans re-exécuter la commande. Sur un projet avec un build NestJS de 8 secondes et une suite Jest de 12 secondes, le cache divise par cinq le temps perçu d’une boucle locale.

nx build api          # première fois : 8s
# modifier un fichier sans rapport
nx build api          # deuxième fois : cache hit, 200ms
nx reset              # vider le cache si besoin

Le mécanisme repose sur un hash calculé à partir des fichiers d’entrée du projet et de ceux de ses dépendances. Si le hash n’a pas bougé, la sortie est restituée. La configuration des entrées par défaut couvre la majorité des cas, mais elle peut s’étendre dans nx.json pour inclure des fichiers de configuration globaux. Cette personnalisation devient utile quand un changement de .env.production doit invalider le cache de build, ce qui n’arrive pas par défaut.

Bonnes pratiques d’organisation des libs

Plutôt qu’une seule lib shared-types qui finit par tout contenir, organiser les libs par scope et par type reste la convention la plus saine. Le scope correspond au domaine métier (billing, users, orders), le type correspond à la nature du code (data-access, feature, ui, util). Une lib billing-data-access contient les appels API et les types ; une lib billing-feature contient la logique métier ; une lib billing-ui contient les composants. Cette discipline permet de pousser des règles ESLint qui interdisent à une lib util d’importer une lib feature.

Les contraintes d’import se déclarent dans .eslintrc.json via la règle @nx/enforce-module-boundaries. Une lib taggée type:util ne peut consommer que d’autres libs type:util. Cette barrière statique vérifiée à chaque lint évite les régressions architecturales quand l’équipe grandit.

Erreurs fréquentes

Erreur Cause Solution
NX 0001 au generate Plugin Nx désaligné avec le core nx migrate latest
Import @acme/shared-types non résolu Path alias absent du tsconfig.base.json Régénérer la lib ou ajouter le path manuellement
CI rebuild tout à chaque commit fetch-depth manquant dans GitHub Actions Ajouter fetch-depth: 0 au checkout
Conflit pnpm-lock entre branches Versions différentes de pnpm Pin packageManager dans package.json
Nx serve api ne reload pas Watch SWC mal configuré Vérifier --watch dans project.json

Ce tableau couvre les pièges qui font perdre une demi-journée à un développeur seul face au workspace. Le plus fréquent reste le rebuild total en CI : il suffit d’oublier fetch-depth: 0 dans une seule action pour annuler tous les bénéfices de nx affected. Une vérification rapide après le premier push consiste à comparer le temps d’exécution sur deux PR successives — si les durées ne diffèrent pas alors qu’une seule lib a été touchée, le calcul d’affectés ne fonctionne pas.

FAQ

Pourquoi pnpm plutôt que npm ou yarn ?
pnpm utilise un store global et des liens symboliques au lieu de copier les paquets dans chaque node_modules. Sur un monorepo de cinq projets, l’install passe de 90 secondes (npm) à 30 secondes (pnpm). Sa stricte résolution des dépendances détecte aussi les imports fantômes que npm laisse passer.

Faut-il activer Nx Cloud ?
Pour un projet en démarrage, non. Le cache local et la détection d’affectés couvrent 90 % du gain. Nx Cloud devient pertinent quand l’équipe dépasse cinq développeurs et que le cache distribué fait gagner trois à cinq minutes par CI.

Peut-on mélanger NestJS et Next.js dans le même workspace ?
Oui, c’est même un usage très courant. Ajouter le plugin @nx/next et générer une app frontend qui consomme la lib shared-types. Le seul piège est de bien isoler les variables d’environnement : NEXT_PUBLIC_* côté front, API_* côté backend, et un fichier .env par projet plutôt qu’un fichier racine partagé.

Comment migrer un projet NestJS existant vers Nx ?
La commande nx init détecte un projet existant et propose une migration progressive qui préserve les scripts npm et la structure des dossiers. C’est plus prudent que de tenter une réécriture en une fois sur un projet en production.

Tutoriels associés

Références

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité