Straffung der Full-Stack-TypeScript-Entwicklung mit Monorepos
Ethan Miller
Product Engineer · Leapcell

Einleitung
In der lebendigen Landschaft der modernen Webentwicklung beinhaltet der Aufbau robuster Full-Stack-Anwendungen oft ein komplexes Zusammenspiel zwischen Front-End-Frameworks und Back-End-Diensten. Wenn Projekte wachsen, kann die Verwaltung unterschiedlicher Codebasen für React-Anwendungen und NestJS-APIs schnell zu erheblichem Mehraufwand führen. Fragmentierung führt zu duplizierten Konfigurationen, inkonsistenten Abhängigkeiten und ineffizienten Build-Prozessen. Hier glänzt der Monorepo-Ansatz und bietet eine einheitliche Entwicklungserfahrung, die diese Herausforderungen direkt angeht. Durch die Konsolidierung verwandter Projekte innerhalb eines einzigen Repositories eröffnen wir leistungsstarke Vorteile in den Bereichen Code-Sharing, Build-Optimierung und allgemeine Entwicklerproduktivität. Dieser Artikel befasst sich damit, wie Tools wie Nx und Turborepo diesen optimierten Workflow speziell für Full-Stack-TypeScript-Projekte (React + NestJS) ermöglichen.
Die Vorteile eines Monorepos verstehen
Bevor wir uns mit den Besonderheiten von Nx und Turborepo befassen, wollen wir ein grundlegendes Verständnis der Kernkonzepte entwickeln, die ihre Stärke im Monorepo-Kontext untermauern.
Monorepo: Eine Entwicklungsstrategie, bei der mehrere verschiedene Projekte (z. B. Front-End, Back-End, gemeinsam genutzte Bibliotheken) in einem einzigen, versionierten Repository gespeichert werden. Dies steht im Gegensatz zu einem "Polyrepo"-Ansatz, bei dem jedes Projekt in seinem eigenen separaten Repository liegt.
Workspace: Im Kontext von Nx und Turborepo bezieht sich ein Workspace auf das Stammverzeichnis Ihres Monorepos, das all Ihre einzelnen Anwendungen und Bibliotheken umfasst.
Anwendung (App): Eine bereitstellbare Einheit innerhalb des Monorepos, wie Ihr React-Front-End oder Ihre NestJS-Back-End-API.
Bibliothek (Lib): Wiederverwendbare Codeblöcke, die über mehrere Anwendungen oder andere Bibliotheken innerhalb des Monorepos gemeinsam genutzt werden können. Dies ist eine Grundlage für die Gewährleistung von Konsistenz und die Reduzierung von Redundanz. Beispiele sind gemeinsam genutzte UI-Komponenten, Datenmodelle, Hilfsfunktionen oder API-Schnittstellen.
Task Runner / Build-System: Tools wie Nx und Turborepo fungieren als intelligente Task Runner. Sie verstehen die Abhängigkeiten zwischen Projekten in Ihrem Monorepo und können verschiedene Entwicklungsaufgaben wie Build, Test, Linting und Serving optimieren.
Abhängigkeitsgraph: Ein entscheidendes Konzept, bei dem diese Tools die Beziehungen zwischen Ihren Projekten (Apps und Bibliotheken) darstellen. Dieser Graph ermöglicht es ihnen, zu bestimmen, welche Projekte nach einer Änderung neu erstellt werden müssen, und Aufgaben in der richtigen Reihenfolge auszuführen.
Verteilte Zwischenspeicherung: Um die Entwicklung weiter zu beschleunigen, nutzen diese Tools die Zwischenspeicherung. Wenn sich eine Abhängigkeit nicht geändert hat, können ihre Build-Ausgaben aus einem Cache (lokal oder remote) wiederverwendet werden, was nachfolgende Builds und Tests erheblich beschleunigt.
Nun untersuchen wir, wie Nx und Turborepo diese Konzepte implementieren, um ein Full-Stack-React- + NestJS-TypeScript-Projekt zu optimieren.
Nx für die Full-Stack-TypeScript-Entwicklung
Nx ist ein leistungsstarkes, erweiterbares Build-System, das die Monorepo-Philosophie verfolgt. Es bietet einen umfassenden Satz von Funktionen zur Verwaltung von Anwendungen im Enterprise-Maßstab. Es bietet starke Meinungen und Code-Generierungsfähigkeiten, was es hervorragend zum schnellen Starten von Projekten und zur Aufrechterhaltung der Konsistenz macht.
Ein Nx Workspace einrichten
Beginnen wir mit der Erstellung eines neuen Nx-Workspaces und dem Hinzufügen unserer React- und NestJS-Anwendungen.
npx create-nx-workspace@latest fullstack-monorepo --preset=react-standalone --appName=frontend --style=css
Dieser Befehl erstellt einen neuen Nx-Workspace namens fullstack-monorepo
mit einer React-Anwendung namens frontend
. Lassen Sie uns nun eine NestJS-Anwendung (oft als API bezeichnet) hinzufügen.
cd fullstack-monorepo npm install -D @nx/nest nx g @nx/nest:application backend --dryRun=false
Nun wird Ihre Workspace-Struktur ungefähr so aussehen:
fullstack-monorepo/
├── apps/
│ ├── backend/ # NestJS-Anwendung
│ └── frontend/ # React-Anwendung
├── libs/ # Gemeinsam genutzte Bibliotheken
├── nx.json # Nx-Konfiguration
├── package.json
└── tsconfig.base.json
Bibliotheken erstellen und teilen
Die wahre Stärke eines Monorepos zeigt sich, wenn Sie beginnen, Code zu teilen. Lassen Sie uns eine gemeinsam genutzte Bibliothek für unsere Datenmodelle oder API-Typen erstellen.
nx g @nx/js:library shared-data --dryRun=false
Dies erstellt libs/shared-data
. In libs/shared-data/src/lib/shared-data.ts
können wir eine Schnittstelle definieren:
// libs/shared-data/src/lib/shared-data.ts export interface User { id: string; name: string; email: string; } export function getUserGreeting(user: User): string { return `Hallo, ${user.name}!`; }
Nun können sowohl unser frontend
als auch unser backend
diese Bibliothek nutzen.
In apps/backend/src/app/app.service.ts
:
// apps/backend/src/app/app.service.ts import { Injectable } from '@nestjs/common'; import { User, getUserGreeting } from '@fullstack-monorepo/shared-data'; // Aus der Bibliothek importieren @Injectable() export class AppService { getData(): { message: string } { const user: User = { id: '1', name: 'Alice', email: 'alice@example.com' }; return { message: getUserGreeting(user) }; } }
Und in apps/frontend/src/app/app.tsx
:
// apps/frontend/src/app/app.tsx import React, { useEffect, useState } from 'react'; import { User, getUserGreeting } from '@fullstack-monorepo/shared-data'; // Aus der Bibliothek importieren export function App() { const [greeting, setGreeting] = useState(''); useEffect(() => { // Beispielverwendung im Front-End const user: User = { id: '2', name: 'Bob', email: 'bob@example.com' }; setGreeting(getUserGreeting(user)); // Beispiel: Daten vom NestJS-Back-End abrufen fetch('/api') .then((res) => res.json()) .then((data) => console.log('Backend-Antwort:', data.message)); }, []); return ( <> <h1>Willkommen, Frontend!</h1> <p>{greeting}</p> </> ); } export default App;
Beachten Sie die Importpfade mit dem Alias @fullstack-monorepo/shared-data
. Nx konfiguriert diese TypeScript-Pfadzuordnungen automatisch in tsconfig.base.json
für nahtlose Importe.
Aufgaben mit Nx ausführen
Nx bietet leistungsstarke Befehle zum Ausführen von Aufgaben in Ihrem Monorepo.
- Beide Anwendungen gleichzeitig ausführen:
nx run-many --target=serve --projects=frontend,backend --parallel
- Alle betroffenen Projekte erstellen:
Dieser Befehl erkennt intelligent Änderungen und erstellt nur Projekte neu, die direkt oder indirekt betroffen sind.nx affected:build
- Tests ausführen:
nx test backend
Die Abhängigkeitsgraph-Analyse von Nx stellt sicher, dass Aufgaben effizient ausgeführt werden, und seine Caching-Mechanismen (lokal und remote) verkürzen die Build-Zeiten drastisch.
Turborepo für Geschwindigkeit und Einfachheit
Turborepo, erworben von Vercel, zeichnet sich durch seinen Fokus auf Geschwindigkeit und Benutzerfreundlichkeit aus. Es ist im Wesentlichen ein Hochleistungs-Build-System für JavaScript- und TypeScript-Monorepos, das schnelle Builds, intelligentes Caching und effiziente Task-Orchestrierung priorisiert. Während Nx eine meinungsstärkere, voll ausgestattete Erfahrung mit Generatoren und Plugins bietet, ist Turborepo agnostischer und konzentriert sich rein auf beschleunigte Builds.
Ein Turborepo Workspace einrichten
Beginnen wir neu, um Turborepo zu demonstrieren.
npx create-turbo-app
Folgen Sie den Eingabeaufforderungen und wählen Sie vorerst ein minimal
-Setup. Dies erstellt eine einfache Workspace-Struktur.
turbo-monorepo/
├── apps/
├── packages/ # Ähnlich wie Nx's libs
├── turborepo.json
├── package.json
Erstellen wir manuell unsere React- und NestJS-Anwendungen.
Für React:
Erstellen Sie in apps/frontend
ein Standard-React-Projekt (z. B. mit Vite oder Create React App).
package.json
für frontend
:
{ "name": "frontend", "version": "1.0.0", "private": true, "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", "typescript": "^5.0.2", "vite": "^4.4.5" } }
Für NestJS:
Erstellen Sie in apps/backend
ein NestJS-Projekt.
package.json
für backend
:
{ "name": "backend", "version": "1.0.0", "private": true, "scripts": { "build": "nest build", "start": "nest start", "start:dev": "nest start --watch" }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "jest": "^29.5.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } }
Gemeinsam genutzte Pakete (Bibliotheken)
In Turborepo liegt gemeinsam genutzter Code im Verzeichnis packages
. Lassen Sie uns packages/shared-data
erstellen.
packages/shared-data/package.json
:
{ "name": "shared-data", "version": "1.0.0", "main": "./src/index.ts", "types": "./src/index.ts", "private": true, "scripts": { "build": "echo \"Kein Build für shared-data nötig, direkte TS-Nutzung.\"" } }
packages/shared-data/src/index.ts
:
// packages/shared-data/src/index.ts export interface User { id: string; name: string; email: string; } export function getUserGreeting(user: User): string { return `Hallo, ${user.name}!`; }
Um dies in unseren Anwendungen zu nutzen, fügen wir shared-data
als Abhängigkeit in ihren jeweiligen package.json
-Dateien hinzu.
In apps/backend/package.json
:
{ // ... andere Felder "dependencies": { "@nestjs/common": "^10.0.0", // ... "shared-data": "workspace:*" // Dies weist npm/yarn/pnpm an, auf das lokale Paket zu verlinken } }
In apps/frontend/package.json
:
{ // ... andere Felder "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "shared-data": "workspace:*" } }
Führen Sie dann npm install
im Stammverzeichnis des Monorepos aus, um diese Pakete zu verknüpfen.
Turborepo-Aufgaben konfigurieren
Das Herzstück von Turborepo ist die Datei turbo.json
im Stammverzeichnis Ihres Workspaces. Sie definiert die Aufgaben (build
, dev
, test
) für jedes Paket und seine Abhängigkeiten.
turbo.json
:
{ "$schema": "https://turbo.build/schema.json", "pipe": { "build": { "cache": true, "dependsOn": ["^build"], "outputs": ["dist/**"] }, "dev": { "cache": false, "persistent": true }, "test": { "cache": true, "outputs": ["coverage/**"] } } }
cache: true
: Aktiviert das Caching für die Aufgabenbuild
undtest
.- `dependsOn: [