Jedes Team, das mehr als zwei Projekte mit geteiltem Code betreibt, steht irgendwann vor derselben Frage: Ein Repository oder mehrere? Beide Antworten haben Konsequenzen. Multi-Repo bedeutet Versionskonflikte, Copy-Paste-Bibliotheken und synchrone Releases, die keiner koordinieren will. Monorepo bedeutet lange CI-Zeiten, unklare Eigentumsverhältnisse und Build-Systeme, die niemand versteht. Nx löst die zweite Kategorie von Problemen — aber nur, wenn man versteht, was es tut und was nicht.
Das Monorepo-Dilemma
Die Idee ist einfach: Alle Projekte in einem Repository. Shared Libraries werden direkt importiert statt als npm-Pakete versioniert. Änderungen an einer Bibliothek sind sofort in allen Konsumenten sichtbar. Kein Diamond-Dependency-Problem, kein Warten auf Releases.
Die Realitaet ist komplizierter. Ab 10 Projekten dauert npm install Minuten. Ab 20 wird npm run build unbrauchbar, weil es alles baut — auch das, was sich nicht geändert hat. Ab 50 Projekten bricht die CI-Pipeline unter der Last zusammen.
Genau hier setzt Nx an. Nicht als Build-Tool, nicht als Framework — sondern als intelligenter Task-Runner, der versteht, welche Projekte voneinander abhaengen und was sich geändert hat.
Was Nx tatsächlich macht
Nx ist kein Ersatz für Webpack, Vite oder tsc. Es orchestriert die vorhandenen Build-Tools. Der Kern besteht aus drei Mechanismen:
- Dependency Graph: Nx analysiert Imports und
package.json-Abhängigkeiten und baut einen gerichteten Graphen aller Projekte. - Task Pipeline: Definiert, in welcher Reihenfolge Tasks ausgeführt werden.
buildvon App X hängt ab vonbuildaller Libraries, die X importiert. - Computation Cache: Speichert Ergebnisse von Tasks. Wenn sich weder Input-Dateien noch Konfiguration geändert haben, wird das gecachte Ergebnis verwendet.
Projektstruktur: Apps und Libraries
Die Standard-Konvention in Nx unterscheidet zwischen apps/ und libs/. Apps sind deploybare Einheiten — ein Frontend, ein API-Server, eine CLI. Libraries enthalten geteilten Code, der von Apps oder anderen Libraries importiert wird.
my-workspace/
├── apps/
│ ├── web-app/ # React Frontend
│ ├── admin-panel/ # Angular Dashboard
│ └── api/ # NestJS Backend
├── libs/
│ ├── shared/
│ │ ├── ui/ # Geteilte UI-Komponenten
│ │ ├── util/ # Hilfsfunktionen
│ │ └── types/ # TypeScript Interfaces
│ ├── feature/
│ │ ├── auth/ # Auth-Logik (Framework-agnostisch)
│ │ └── billing/ # Billing-Domain
│ └── data-access/
│ ├── user-api/ # API-Client für Users
│ └── billing-api/ # API-Client für Billing
├── nx.json
└── tsconfig.base.json Die Aufteilung in shared/, feature/ und data-access/ ist kein Zufall. Nx erzwingt über Module Boundaries, dass Abhängigkeiten nur in eine Richtung fliessen: feature darf data-access und shared importieren, aber nicht umgekehrt. data-access darf shared importieren, aber nicht feature.
{
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:data-access", "type:shared"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:data-access", "type:shared"]
},
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": ["type:shared"]
},
{
"sourceTag": "type:shared",
"onlyDependOnLibsWithTags": ["type:shared"]
}
]
}
]
} Das ist einer der größten Vorteile von Nx. Ohne diese Regeln wird jedes Monorepo innerhalb von Monaten zum Big Ball of Mud — alles importiert alles, und niemand traut sich mehr, etwas zu ändern.
nx affected: Nur bauen, was sich geändert hat
Der wichtigste Befehl im Nx-Alltag ist nx affected. Er vergleicht den aktuellen Stand mit einem Referenz-Branch (typischerweise main) und führt Tasks nur für betroffene Projekte aus.
# Nur betroffene Projekte testen
nx affected -t test
# Nur betroffene Projekte bauen
nx affected -t build
# Betroffene Projekte anzeigen (ohne Task)
nx show projects --affected
# Gegen einen bestimmten Branch vergleichen
nx affected -t lint --base=develop --head=HEAD In der CI-Pipeline spart das enorm Zeit. Eine Änderung an libs/shared/ui löst Builds für alle Apps aus, die diese Library importieren. Eine Änderung an libs/feature/billing betrifft nur die Apps, die Billing nutzen. Eine Änderung an einer Markdown-Datei löst gar nichts aus.
Computation Caching: Lokal und Remote
Nx cached die Ergebnisse aller Tasks. Der Cache-Key besteht aus den Input-Dateien, der Task-Konfiguration und den Umgebungsvariablen. Wenn nichts davon sich geändert hat, wird das Ergebnis aus dem Cache geliefert — inklusive Terminal-Output und erzeugter Dateien.
Lokal funktioniert das sofort. Remote Caching über Nx Cloud (oder self-hosted mit Nx Enterprise) teilt den Cache zwischen CI-Runnern und Entwicklern. Wenn ein Kollege libs/shared/util bereits gebaut hat, bekommt jeder andere den Build in Millisekunden statt Minuten.
Code Generators und Workspace Plugins
Nx bringt Generatoren mit, die neue Projekte und Libraries nach einem konsistenten Schema anlegen. Das klingt trivial, aber in einem Workspace mit 40 Libraries ist es ein Unterschied, ob jede Library ihr eigenes Konfigurationsformat erfindet oder ob alle dasselbe Pattern verwenden.
# Neue React-Library erstellen
nx g @nx/react:library shared-ui --directory=libs/shared/ui
# Neue NestJS-Applikation
nx g @nx/nest:application api --directory=apps/api
# Eigenen Generator erstellen (Workspace Plugin)
nx g @nx/plugin:generator my-custom-lib \
--project=my-workspace-plugin
# Dry-Run: Vorschau ohne Dateien zu erzeugen
nx g @nx/react:library feature-auth \
--directory=libs/feature/auth --dry-run Der --dry-run-Schalter ist unterschätzt. Er zeigt genau, welche Dateien erzeugt und verändert werden — ohne etwas zu schreiben. In einem großen Workspace vermeidet das Überraschungen.
Eigene Workspace Plugins gehen einen Schritt weiter. Damit lassen sich Generatoren und Executors erstellen, die firmenspezifische Konventionen erzwingen: Standard-CI-Konfiguration, einheitliche Linting-Regeln, vordefinierte Abhängigkeiten.
Integration mit Frameworks
Nx ist Framework-agnostisch, bringt aber offizielle Plugins für die gaengigen Frameworks mit. Angular hat die tiefste Integration — historisch bedingt, da Nx aus dem Angular-Ökosystem stammt. React, Vue, Node.js und NestJS sind ebenfalls erstklassig unterstützt.
Die Plugins liefern:
- Executors: Vorkonfigurierte Build-, Test- und Serve-Kommandos
- Generatoren: Schema-basierte Codegenerierung (Komponenten, Services, Module)
- Migrations: Automatische Updates bei Versionswechseln
Ein gemischter Workspace mit Angular-Frontend, React-Admin-Panel und NestJS-Backend ist ein gaengiges Setup. Der geteilte Code in libs/shared/types wird von allen drei Frameworks importiert. TypeScript Path Mappings in tsconfig.base.json machen das möglich, ohne Pakete zu publizieren.
Wann Monorepo falsch ist
Nicht jedes Team profitiert von einem Monorepo. Es gibt klare Kontraindikationen:
Team-Größe unter drei Entwicklern. Der Overhead von Nx — Konfiguration, Module Boundaries, Plugin-Wartung — lohnt sich erst, wenn mehrere Entwickler parallel an verschiedenen Projekten arbeiten. Ein Zwei-Personen-Team mit einem Frontend und einem Backend faehrt mit zwei Repositories einfacher.
Organisatorisch getrennte Teams. Wenn das Frontend-Team und das Backend-Team unterschiedliche Release-Zyklen haben, unterschiedliche Sprachen verwenden und keinen Code teilen, erzwingt ein Monorepo Kopplung, die organisatorisch nicht existiert. Conway’s Law lässt sich nicht mit Tooling überlisten.
Unterschiedliche Technologie-Stacks. Ein Monorepo mit TypeScript-Frontend und Rust-Backend ergibt wenig Sinn. Nx versteht JavaScript/TypeScript. Für polyglotte Setups gibt es Bazel oder Pants — aber deren Lernkurve ist steil.
Migration: Von Multi-Repo zu Nx
Die Migration muss nicht Big-Bang sein. Nx unterstützt einen inkrementellen Ansatz:
- Workspace erstellen und die bestehenden Projekte als Apps hinzufügen — zunächst ohne geteilte Libraries.
- Build-Konfiguration migrieren. Bestehende Webpack- oder Vite-Configs bleiben erhalten. Nx-Executors wrappen sie nur.
- Geteilten Code extrahieren. Duplicate Code zwischen Apps identifizieren und in Libraries verschieben. Das ist der schwierigste Schritt — hier zeigt sich, ob die Codebasis tatsächlich geteilte Konzepte hat oder nur Copy-Paste-Duplikate.
- Module Boundaries einführen. Erst wenn die Library-Struktur stabil ist, die ESLint-Regeln aktivieren.
- CI-Pipeline umstellen auf
nx affectedstattnpm run build --workspaces.
Die ersten drei Schritte kann man in einer Woche schaffen. Schritt vier und fuenf dauern länger, weil sie Konventionen im Team erfordern.
Ein pragmatischer Tipp: Die erste Library, die man extrahiert, sollte shared/types sein — TypeScript Interfaces, die Frontend und Backend teilen. Das hat den höchsten ROI bei geringstem Risiko.
Fazit
Nx löst ein reales Problem: große Codebasen mit vielen Projekten handhabbar zu machen. Task-Graphen, Affected-Analysen und Computation Caching sind keine Marketing-Features — sie machen den Unterschied zwischen einer CI-Pipeline, die 5 Minuten läuft, und einer, die 45 Minuten läuft.
Aber Nx ist kein Allheilmittel. Es setzt voraus, dass die Projekte im Workspace tatsächlich zusammengehören, dass das Team die Konventionen trägt und dass die Investition in Tooling-Konfiguration sich durch geteilten Code amortisiert. Wer diese Voraussetzungen erfüllt, bekommt ein System, das mit dem Team skaliert. Wer sie nicht erfüllt, hat nur ein komplexeres Build-System.