Guide pratique pour structurer un monorepo Angular avec Nx : libs partagées, boundaries, caching et workflows CI efficaces.
Pourquoi un monorepo Angular avec Nx ?
Un monorepo regroupe plusieurs applications et bibliothèques dans un seul dépôt Git. Avec Nx, cela apporte une cohérence d'outillage, un partage de code sans publication npm intermédiaire et une gouvernance centralisée des versions de dépendances.
Contrairement à un multirepo, chaque changement cross-app est atomique : une seule PR peut mettre à jour une lib partagée et toutes les apps qui en dépendent simultanément.
Les bénéfices principaux de Nx pour Angular :
- Builds et tests incrémentaux : seul l'affecté est recompilé
- Cache local et distribué (Nx Cloud) pour des CI ultra-rapides
- Générateurs officiels
@nx/angularalignés sur Angular CLI - Dependency graph visuel (
nx graph) - Enforced boundaries pour éviter les couplages involontaires
Créer un workspace Nx Angular
La commande create-nx-workspace initialise le dépôt avec la stack Angular
préconfigurée — bundler Vite ou esbuild, ESLint, Jest — selon vos choix interactifs.
# Créer le workspace (mode interactif)
npx create-nx-workspace@latest my-org --preset=angular-monorepo
# Structure générée
my-org/
├── apps/
│ └── my-app/ # Application Angular principale
├── libs/ # Bibliothèques partagées
├── nx.json # Configuration Nx globale
├── tsconfig.base.json # Chemins partagés
└── package.json
# Ajouter une deuxième application
nx generate @nx/angular:application admin-app --directory=apps/admin-app
# Ajouter une bibliothèque partagée
nx generate @nx/angular:library ui-button --directory=libs/ui/button
tsconfig.base.json et paths
Nx configure automatiquement les paths TypeScript pour chaque lib générée.
Importez @my-org/ui/button directement, sans chemin relatif :
// Dans apps/my-app/src/app/app.component.ts
import { ButtonComponent } from '@my-org/ui/button';
Après génération, lancez nx serve my-app pour démarrer votre app.
Nx détecte automatiquement le projet si vous êtes dans son répertoire.
Structure des libs : feature, ui, data-access, util
Nx recommande quatre types de bibliothèques aux responsabilités distinctes. Ce modèle évite les dépendances circulaires et facilite la maintenance à grande échelle.
libs/
├── feature/
│ └── user-profile/ # Smart component + routing
├── ui/
│ └── button/ # Presentational components (pas de services HTTP)
│ └── form-field/
├── data-access/
│ └── user/ # Services HTTP, stores, signals
│ └── auth/
└── util/
└── date-helpers/ # Pure functions, pipes, validators
Voici un exemple de lib data-access avec un signal store :
// libs/data-access/user/src/lib/user.store.ts
import { signalStore, withState, withMethods } from '@ngrx/signals';
import { inject } from '@angular/core';
import { UserService } from './user.service';
export const UserStore = signalStore(
{ providedIn: 'root' },
withState({ users: [], loading: false }),
withMethods((store, service = inject(UserService)) => ({
async loadUsers() {
patchState(store, { loading: true });
const users = await service.getAll();
patchState(store, { users, loading: false });
},
})),
);
ui ne doivent jamais importer de libs
data-access. Les libs feature orchestrent les deux. Cette hiérarchie
se formule dans les boundaries ESLint.
Enforced boundaries avec ESLint
La règle @nx/enforce-module-boundaries analyse statiquement les imports à
chaque lint et bloque les couplages interdits entre couches ou entre domaines.
Taggez chaque projet dans son project.json :
// libs/ui/button/project.json
{
"name": "ui-button",
"tags": ["scope:shared", "type:ui"]
}
// libs/data-access/user/project.json
{
"name": "data-access-user",
"tags": ["scope:user", "type:data-access"]
}
Puis définissez les règles dans eslint.config.js à la racine :
// eslint.config.js (racine)
{
rules: {
'@nx/enforce-module-boundaries': ['error', {
depConstraints: [
// ui ne peut importer que util
{ sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:util'] },
// data-access ne peut importer que util
{ sourceTag: 'type:data-access', onlyDependOnLibsWithTags: ['type:util'] },
// feature peut importer ui, data-access, util
{ sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:ui', 'type:data-access', 'type:util'] },
// scope isolation : un domaine n'importe pas l'autre
{ sourceTag: 'scope:user', notDependOnLibsWithTags: ['scope:auth'] },
],
}],
},
}
Vérifier les violations
Lancez nx lint --all pour auditer l'ensemble du monorepo. En CI,
nx affected --target=lint ne teste que les projets modifiés.
Nx affected et cache CI/CD
Nx construit un graphe de dépendances complet. La commande nx affected
calcule automatiquement quels projets sont impactés par un diff Git et n'exécute que ceux-là.
# Lancer uniquement les tests affectés par la PR courante
nx affected --target=test --base=origin/main
# Construire uniquement les apps affectées
nx affected --target=build --base=origin/main --parallel=3
# Visualiser le graphe d'impact
nx affected:graph --base=origin/main
Exemple de pipeline GitHub Actions optimisé :
# .github/workflows/ci.yml
name: CI
on: [pull_request]
jobs:
affected:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
# Cache Nx local
- uses: actions/cache@v4
with:
path: .nx/cache
key: nx-${{ hashFiles('**/package-lock.json') }}
- run: npx nx affected --target=lint --base=origin/main --parallel=3
- run: npx nx affected --target=test --base=origin/main --parallel=3
- run: npx nx affected --target=build --base=origin/main --parallel=3
nx connect — le plan gratuit couvre la plupart des projets open source.
Checklist monorepo scalable
Avant de merger votre premier projet en monorepo Nx, validez chacun de ces points pour garantir une base saine à long terme.
- Workspace créé avec
create-nx-workspacepresetangular-monorepo - Chaque lib tagguée
scope:xxx+type:xxxdansproject.json - Règle
@nx/enforce-module-boundariesactive et couvrant tous les types tsconfig.base.jsoncentralise tous lespathsde libs- CI utilise
nx affectedavec--base=origin/main - Cache Nx local configuré dans GitHub Actions (action
actions/cache) - Aucun import relatif cross-lib (utiliser uniquement les alias
@my-org/...) - Nx Cloud connecté pour partager le cache entre runners parallèles