Startseite / Artikel

Wo Turborepo in einem NestJS Monorepo Platz findet und wann man darauf verzichten sollte

Wie Turborepo, pnpm Workspaces und NestJS die Arbeit in einem TypeScript-Monorepo aufteilen: gemeinsame Pakete, Task-Graphen, Caching, Kopplungsrisiken und wann man darauf verzichten sollte.

Verlag: REACTAPP.TOP15 Min. Lesezeit3349 Wörter

Quellen: Turborepo + NestJS: The Monorepo Architecture That Makes Large TypeScript Systems Manageable

Ein NestJS-Backend beginnt in der Regel als eine einzige API mit einem package.json, einer node_modules-Ordner und einem CI-Pipeline. Mit dem Wachstum des Produkts werden Authentifizierung, Bestellungen, Zahlungen, Lagerverwaltung und Benachrichtigungen in separate Services aufgeteilt, wodurch sich die operative Frage von „Wie skalieren wir diesen Service?“ auf „Wie verwalten wir sieben TypeScript-Anwendungen, ohne dieselbe Konfiguration, denselben Typen- und Pipeline-Inhalt siebenmal kopieren zu müssen?“ verschiebt. Dieser Artikel erläutert, welchen Beitrag ein Monorepo, pnpm und Turborepo jeweils leisten, wie Task-Graphen und Caches Arbeit sparen, was geteilt werden kann und wann die Einrichtungskosten höher sind als der Nutzen.

Haben Sie stets eine Grenze im Hinterkopf. Turborepo erstellt keine Microservices, ersetzt NestJS nicht und macht eine Architektur von selbst skalierbar. Es löst ein schmaleres Problem: die Koordination von Build-Aufgaben in vielen Paketen innerhalb eines Repositoriums.

Von einem Service zu vielen Repositorien

Das Repositorium einer wachsenden Plattform könnte letztendlich einen Ordner pro Service enthalten:

auth-service/
order-service/
payment-service/
inventory-service/
notification-service/
shipment-service/
api-gateway/

Die Aufteilung in Services hilft bei der Skalierbarkeit und der Zuordnung der Verantwortlichkeiten, doch jeder Service bringt nun seine eigenen Abhängigkeiten, Konfigurationen, Build-Prozesse, Tests, Lint-Regeln sowie Pipelines mit sich.

Die Koordinationskosten eines Polyrepos

Bilden Sie sich ein typisches E-Commerce-System vor, in dem eine Gateway-Schicht die Authentifizierungs-, Bestell- und Produktservices abdeckt, der Bestellservice auf Zahlungssysteme und Lagerbestände zugreift und die Zahlungsabwicklung Benachrichtigungen auslöst:

                    API Gateway
                         |
       ┌─────────────────┼─────────────────┐
       ↓                 ↓                 ↓
     Auth              Order            Product
                         |
                ┌────────┴────────┐
                ↓                 ↓
             Payment          Inventory
                |
                ↓
           Notification

Eine gängige Erstmaßnahme besteht darin, jedem Service sein eigenes Repository zur Verfügung zu stellen – der sogenannte polyrepo-Ansatz:

github.com/company/auth-service
github.com/company/order-service
github.com/company/product-service
github.com/company/payment-service
github.com/company/inventory-service
github.com/company/notification-service

Das ist eine legitime Architektur. Probleme entstehen, wenn man in jedes Repository schaut und überall dieselben Grundstrukturen wiederfindet:

package.json
tsconfig.json
eslint.config.js
Dockerfile
CI configuration
README.md
node_modules

Und noch schlimmer: dieselben Arten von Code, die gleichzeitig von mehreren Services benötigt werden:

DTOs
TypeScript types
logging utilities
configuration
authentication helpers
database utilities
Kafka/RabbitMQ abstractions
testing utilities

Jeder Änderung an gemeinsam genutztem Material wird nun zu einer Synchronisierungsaufgabe. Wenn sich ein gemeinsamer Typ ändert, welche Repositories müssen aktualisiert werden? Wenn eine Abhängigkeit geändert wird, welche Services benötigen den Upgrade? Wenn sich eine Lint-Regel oder ein CI-Ablauf ändert, wie viele Kopien müssen bearbeitet und auf dem neuesten Stand gehalten werden? Die Anzahl der Services ist nicht das eigentliche Problem – die Koordination schon.

Ein Monorepo ist eine Strategie; Turborepo ist ein Tool

Die Alternative besteht darin, alle Anwendungen und gemeinsam genutzten Code in einem einzigen Repository zu halten, wobei die Anwendungen unter apps/, interne Bibliotheken unter packages/ und die Konfiguration des Arbeitsbereichs sowie der Aufgaben im Wurzelverzeichnis gespeichert werden:

ecommerce-platform/
│
├── apps/
│   ├── api-gateway/
│   ├── auth-service/
│   ├── order-service/
│   ├── product-service/
│   ├── payment-service/
│   ├── inventory-service/
│   └── notification-service/
│
├── packages/
│   ├── types/
│   ├── logger/
│   ├── config/
│   ├── database/
│   └── messaging/
│
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

Diese Struktur ist ein Monorepo – eine Entscheidung darüber, wo der Code gespeichert wird. Turborepo ist ein Tool, das die Arbeit darin koordiniert. Man kann eines ohne das andere haben.

Was Turborepo tatsächlich tut

Turborepo ist ein Build-System für JavaScript- und TypeScript-Monorepos. Es orchestriert Aufgaben, die in mehreren Paketen gleichzeitig vorhanden sind, wie zum Beispiel:

build
test
lint
type-check
dev

Ohne es bedeutet das Kompilieren mehrerer Dienste, jeden davon manuell aufrufen zu müssen. Zuerst der Auth-Dienst:

cd apps/auth-service
pnpm build

danach die anderen nacheinander (der Quellcode führt zwei Befehle in einer Zeile aus; jedes cd und pnpm build sollte in seiner eigenen Zeile stehen):

cd ../order-service
pnpm buildcd ../payment-service
pnpm build

Mit Turborepo deklarieren Sie die Aufgaben einmal und führen von der Wurzel aus einen einzigen Befehl aus:

turbo build

Dann ermittelt es, welche Aufgaben benötigt werden, welche gleichzeitig ausgeführt werden können und welche Ergebnisse aus dem Cache wiederhergestellt statt neu berechnet werden können.

Strukturierung von NestJS-Anwendungen und gemeinsamen Paketen

Eine für die Produktion optimierte Version des Repositoriums gibt jeder Nest-Anwendung ihr eigenes src/-Verzeichnis sowie package.json, sodass jede Anwendung ein eigenständiges Workspace-Paket darstellt:

ecommerce-platform/
│
├── apps/
│   │
│   ├── api-gateway/
│   │   ├── src/
│   │   └── package.json
│   │
│   ├── auth-service/
│   │   ├── src/
│   │   └── package.json
│   │
│   ├── order-service/
│   │   ├── src/
│   │   └── package.json
│   │
│   └── payment-service/
│       ├── src/
│       └── package.json
│
├── packages/
│   │
│   ├── types/
│   ├── logger/
│   ├── config/
│   ├── database/
│   └── messaging/
│
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

Konzeptionell gibt es zwei Ebenen: Anwendungen, die NestJS-Dienste mit Geschäftslogik sind, und Pakete, die wiederverwendbaren Code enthalten.

                Monorepo
                    |
          ┌─────────┴─────────┐
          ↓                   ↓
        Apps              Packages
          |                   |
       NestJS            Shared Code

Anwendungen implementieren Funktionen, Pakete liefern gemeinsame Bausteine, und Turborepo koordiniert die Aufgaben in beiden Bereichen.

Interne Pakete als einzige Quelle der Wahrheit

Der stärkste alltägliche Vorteil eines Monorepos sind interne Pakete. Ein Types-Paket kann beispielsweise die Modelle enthalten, auf denen mehrere Dienste sich einigen:

packages/
└── types/
    ├── src/
    │   ├── user.ts
    │   ├── order.ts
    │   ├── payment.ts
    │   └── pagination.ts
    └── package.json

Jeder Nest-Dienst kann anschließend über den Paketnamen daraus importieren, wobei dieser über das Workspace und nicht über ein Registry gelöst wird:

import { OrderStatus } from '@repo/types';

anstatt denselben Enum in jeden Dienst einzufügen und zu hoffen, dass die Kopien niemals abweichen:

enum OrderStatus {
  PENDING = 'pending',
  PAID = 'paid',
  CANCELLED = 'cancelled',
}

Wenn dem Enum ein Wert hinzugefügt wird, markiert der Compiler alle betroffenen Stellen in derselben Änderung.

Die gemeinsame Nutzung von Verträgen bedeutet nicht automatisch gemeinsames Eigentum

In einem Monorepo ist die gemeinsame Nutzung einfach – genau deshalb ist Disziplin erforderlich. Die Domänenlogik des Bestelldienstes gehört zum Bestelldienst selbst:

order-service
      ↓
order-domain

Sie sollte nicht heimlich in ein Paket migrieren:

packages/
└── order-domain/

das dann von anderen Diensten importiert wird:

payment-service
inventory-service
notification-service

Sobald der Zahlungs-, Lager- und Benachrichtigungscode von den internen Strukturen des Bestelldomänen abhängt, kann eine Änderung der Bestellregeln drei unabhängige Implementierungen beeinträchtigen. Die Faustregel lautet: Teilen Sie Infrastruktur und Verträge bewusst – teilen Sie aber niemals standardmäßig das Geschäftsbesitzrecht. Geeignete Kandidaten für gemeinsam genutzte Pakete sehen so aus:

@repo/types
@repo/logger
@repo/config
@repo/testing
@repo/messaging

Die Geschäftslogik sollte normalerweise im Service verbleiben, der sie besitzt. Für eine ausführlichere Behandlung dieses Abwägungsproblems siehe Duplikation versus Kopplung: Entscheidung darüber, wann gemeinsam genutzter Code erforderlich ist.

Der Abhängigkeitsgraph entscheidet, was ausgeführt wird

Turborepo liest die in den package.json-Dateien jedes Arbeitsbereichs deklarierten Abhängigkeitsbeziehungen. In diesem Beispiel sind beide Dienste von dem types-Paket abhängig, und der order-Dienst hängt außerdem von einem Datenbankpaket ab:

                  @repo/types
                  /       \
                 /         \
                ↓           ↓
        auth-service    order-service
                              |
                              ↓
                        @repo/database

Weil Turborepo dieses Abhängigkeitsnetzwerk kennt, muss bei Bearbeitung einer Datei innerhalb des order-Dienstes:

apps/order-service/src/orders/order.service.ts

kein nicht verwandter Dienst neu kompiliert werden:

notification-service

Mit Filtern (zum Beispiel turbo build --filter=...) kann eine Ausführung auf geänderte Pakete und deren Abhängigkeiten beschränkt werden.

Caching: Arbeit, die bereits erledigt wurde, überspringen

Zum ersten Mal bei einer Kompilierung:

turbo build

wird jede erforderliche Aufgabe tatsächlich ausgeführt:

auth-service        → BUILD
order-service       → BUILD
payment-service     → BUILD
inventory-service   → BUILD

Turborepo erfasst die Eingaben jeder Aufgabe (Quelldateien, Abhängigkeiten, Konfiguration, relevante Umgebungsvariablen) sowie die Ausgaben und Protokolle. Führen Sie denselben Befehl erneut aus, ohne etwas Relevantes zu ändern:

turbo build

und das Ergebnis sieht so aus:

auth-service        → CACHE HIT
order-service       → CACHE HIT
payment-service     → CACHE HIT
inventory-service   → CACHE HIT

Die Ausgaben wurden aus dem Cache wiederhergestellt. Damit das funktioniert, muss Turborepo wissen, welche Dateien eine Aufgabe erzeugt – das wird durch die outputs-Einstellung in turbo.json angegeben (bei einem Nest-Build sind es in der Regel dist/**). Eine Aufgabe mit undeklarierten Ausgaben kann einen Cache-Hit melden, während Ihnen die Build-Dateien fehlen.

Warum CI am meisten profitiert

Eine naive Pipeline wiederholt die gesamte Arbeit bei jedem Push:

Git Push
   ↓
Install dependencies
   ↓
Build everything
   ↓
Test everything
   ↓
Lint everything
   ↓
Deploy

Mit Turborepo im Pipeline wird jede Aufgabe zunächst mit dem Graphen und dem Cache abgeglichen, und nur diejenigen Aufgaben, deren Eingaben sich geändert haben, werden ausgeführt:

                Git Push
                    ↓
                   CI
                    ↓
               Turborepo
                    ↓
          ┌─────────┴─────────┐
          ↓                   ↓
      Dependency          Task Graph
       Analysis               ↓
                          Cache Check
                              ↓
                    ┌─────────┴─────────┐
                    ↓                   ↓
                 Cached              Execute
                  Tasks                Tasks

In großen Repositorien wird so viel überflüssige CI-Arbeit beseitigt.

Lokaler Cache versus entfernter Cache

Es gibt zwei Ebenen des Cachens:

  • Lokaler Cache. Ihr eigenes Gerät wiederverwendet die Ergebnisse von Aufgaben, die es bereits ausgeführt hat, was die tägliche Entwicklung beschleunigt.
  • Entfernter Cache. Ein gemeinsamer Cache ermöglicht es Entwicklern und CI-Maschinen, die Ergebnisse voneinander zu wiederverwenden.

Die entfernte Konfiguration sieht so aus:

Developer A
     │
     ↓
Remote Cache
     ↑
     │
Developer B
     ↑
     │
    CI

Falls ein Entwickler bereits einen Service erstellt hat und das CI später dieselben Eingabedaten für eine Aufgabe erhält, lädt das CI das Ergebnis herunter anstelle eines erneuten Builds. Behandeln Sie den entfernten Cache als Teil Ihrer Lieferkette: Kontrollieren Sie, wer darauf schreiben darf, denn ein verseuchtes Artefakt würde an alle verteilt werden.

Ausführung unabhängiger Aufgaben parallel

Einige Services sind nicht von den Build-Ergebnissen anderer Services abhängig:

auth-service
order-service
product-service

Die Ausführung erfolgt sequenziell, wobei jede Aufgabe auf die vorherige wartet:

auth → finish
       ↓
order → finish
         ↓
product

Turborepo kann sie stattdessen unter Berücksichtigung der verfügbaren CPU-Kapazitäten gleichzeitig ausführen:

        ┌── auth ────┐
        │            │
turbo ──┼── order ───┼──→ Done
        │            │
        └── product ─┘

Beschreibung der Beziehungen zwischen Aufgaben

Dieselbe Orchestrierung gilt für alle Arten von Aufgaben, nicht nur für Builds:

turbo build
turbo test
turbo lint
turbo typecheck

Beziehungen werden in turbo.json deklariert. In der untenstehenden Konfiguration bedeutet "^build" „erst die Abhängigkeiten dieses Pakets kompilieren“, sodass gemeinsam genutzte Pakete vor den Diensten, die sie importieren, kompiliert werden. Die test-Aufgabe hängt von der build-Aufgabe ab – ohne Bindestrich – wodurch die Kompilierung desselben Pakets gemeint ist. lint und typecheck haben keine Abhängigkeiten und können daher sofort parallel ausgeführt werden.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {},
    "typecheck": {}
  }
}

Sie beschreiben, wie sich die Aufgaben voneinander abhängen, und Turborepo erstellt daraufhin einen Ausführungsplan. In einem echten Projekt würden Sie außerdem, wie oben beschrieben, outputs zu cachbaren Aufgaben hinzufügen und sicherstellen, dass jeder Aufgabennamen mit einem Skript in den package.json-Dateien der Workspace-Pakete übereinstimmt.

pnpm und Turborepo erledigen unterschiedliche Aufgaben

Diese Kombination tritt so häufig auf, dass sie wie ein einziges Werkzeug erscheinen kann:

Turborepo + pnpm

Es handelt sich dabei um getrennte Schichten. pnpm verwaltet Pakete und Arbeitsumgebungen: Es installiert Abhängigkeiten, verknüpft interne Pakete und stellt eine strenge Struktur für node_modules sicher. Turborepo orchestriert Aufgaben und speichert deren Ergebnisse auf dieser Arbeitsumgebung.

pnpm
              │
      Workspace Management
              │
              ↓
        ┌─────────────┐
        │  Monorepo   │
        └──────┬──────┘
               │
               ↓
          Turborepo
               │
       ┌───────┼────────┐
       ↓       ↓        ↓
     Build    Test     Lint

Um pnpm genauer unter die Lupe zu nehmen, lesen Sie npm vs pnpm: Speicher, Geschwindigkeit und praktische Abwägungen.

Warum nicht einfach den integrierten Monorepo-Modus von Nest verwenden?

Nest CLI verfügt über seinen eigenen Monorepo-Modus mit Anwendungen und Bibliotheken, weshalb ein Einsatz von Turborepo nur dann sinnvoll ist. Der Unterschied liegt im Umfang: Nests Arbeitsumgebung versteht nur Nest-Projekte, während Turborepo mit allen JavaScript- oder TypeScript-Paketen in der Arbeitsumgebung arbeiten kann. Sobald das Repository mehr als nur Nest-Dienste enthält – beispielsweise Frontends sowie einfache Node-Worker –

apps/
├── web/              # Next.js
├── admin/            # Next.js
├── api-gateway/      # NestJS
├── order-service/    # NestJS
└── worker/           # Node.js

zusätzlich zu gemeinsamen UI-, Typen- und Konfigurationspaketen:

packages/
├── ui/
├── types/
├── config/
└── eslint-config/

wird nicht mehr ein Nest-Projekt verwaltet, sondern eine TypeScript-Plattform – genau hier wird Turborepo attraktiv.

Turborepo, Nx oder die Nest CLI

Es gibt keine universell richtige Wahl. Als allgemeine Orientierung: Der Monorepo-Modus von Nest ist die einfachste Option, wenn alles aus Nest besteht. Turborepo stellt eine vergleichsweise schmale Schicht für die Ausführung von Aufgaben und Caching über den Arbeitsbereichen des Paketmanagers bereit. Nx bietet außerdem einen Projektgraphen und Caching sowie eine größere Anzahl an Plugins, Code-Generatoren und Konventionen – wofür einige Teams Wert legen, während andere es als überladen empfinden. Wählen Sie entsprechend Ihren tatsächlichen Anforderungen, nicht danach, welches Tool am meisten Aufmerksamkeit in den sozialen Medien erhält.

Wann Turborepo überflüssig ist

Nehmen wir an, das gesamte System besteht aus einer Nest API, die von PostgreSQL und Redis unterstützt wird:

NestJS API
   ↓
PostgreSQL
   ↓
Redis

bei einem kleinen Team mit nur einer deploybaren Anwendung:

1 application
5 developers
1 deployment

Turborepo ist vermutlich unnötig. Ein einfaches Nest-Repository ist einfacher, und die Einführung all dieser Komponenten:

Turborepo
pnpm workspace
multiple packages
complex task graph

Es entsteht eine kognitive Belastung, die sich nicht auszahlt. Tools verdienen ihren Platz erst dann, wenn das Problem ihre Verwendung rechtfertigt.

Wann sich die Investition lohnt

Die Situation ändert sich, wenn mehrere dieser Bedingungen gleichzeitig erfüllt sind:

Multiple applications
        +
Shared packages
        +
Shared tooling
        +
Repeated CI tasks
        +
Large TypeScript codebase

Ein typisches Szenario ist eine Plattform mit Next.js-Frontends, Nest-APIs sowie Hintergrundprozessen, die alle auf gemeinsamen Paketen basieren:

                    Platform
                       │
        ┌──────────────┼──────────────┐
        ↓              ↓              ↓
    Next.js         NestJS         Workers
      Web             APIs           Jobs
        │              │              │
        └──────────────┼──────────────┘
                       ↓
                  Shared Packages

Eine ausgereifte Struktur mit klaren Verantwortlichkeiten

Eine ausgereifte Nest-Plattform könnte neben Anwendungen und Paketen auch einen separaten Bereich für die Infrastruktur hinzufügen:

platform/
│
├── apps/
│   ├── api-gateway/
│   ├── auth-service/
│   ├── user-service/
│   ├── order-service/
│   ├── inventory-service/
│   ├── payment-service/
│   └── notification-service/
│
├── packages/
│   ├── config/
│   ├── logger/
│   ├── types/
│   ├── database/
│   ├── messaging/
│   └── testing/
│
├── infrastructure/
│   ├── docker/
│   └── deployment/
│
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

Jedes Tool hat eine einzige Aufgabe. NestJS ist das Anwendungsframework:

NestJS
   ↓
Application framework

Die verbleibenden Schichten sind wie folgt aufgeteilt (im Quellcodeausschnitt wurden mehrere Einträge zusammengefasst; man kann dies als pnpm für Abhängigkeiten und Arbeitsumgebungen, Turborepo für Aufgabenorchestrierung und Caching, Docker für Containerisierung, Kafka oder RabbitMQ für asynchrone Nachrichtenübertragung sowie PostgreSQL oder Redis für Daten und Caching verstehen):

pnpm
   ↓
Dependency & workspace managementTurborepo
   ↓
Task orchestration + cachingDocker
   ↓
ContainerizationKafka / RabbitMQ
   ↓
Asynchronous communicationPostgreSQL / Redis
   ↓
Data & caching

Turborepo ist weder Ihre Infrastruktur noch Ihr Nachrichtenbroker oder Ihre Bereitstellungsplattform. Es handelt sich um die Build- und Aufgabenschicht, die das Monorepo umgibt.

Ein Repository bedeutet nicht eine Bereitstellung

Eine häufige Sorge ist, dass die Zusammenfassung aller Elemente in einem Repository die Unabhängigkeit zerstört, die Microservices eigentlich bieten sollen. Das ist nicht der Fall. Wo der Code liegt und wie er bereitgestellt wird, sind getrennte Entscheidungen. Ein einziges Repository kann viele Dienste enthalten:

One Git repository
       │
       ├── auth-service
       ├── order-service
       ├── payment-service
       └── notification-service

Wobei jedes Element weiterhin als eigenständiger Container bereitgestellt wird:

auth-service        → Container A
order-service       → Container B
payment-service     → Container C
notification        → Container D

und nach eigenen Regeln skaliert:

order-service × 10
payment-service × 5
notification × 3

Ein Monorepo ist kein Monolith, und ein Polyrepo garantiert nicht automatisch Microservices. Es handelt sich dabei um unabhängige Konzepte. Was man wirklich benötigt, ist ein CI-System, das nur die Dienste bereitstellt, deren Code oder Abhängigkeiten sich geändert haben – was durch den Abhängigkeitsgraphen möglich wird.

Das verborgene Risiko: ein gemeinsames Paket, das alles aufnimmt

Einfaches Teilen hat zwei Seiten. Oft beginnt es harmlos mit einem einzigen Paket:

packages/common

das allmählich Hilfsfunktionen für jedes Gebiet sammelt:

packages/common
├── AuthUtils
├── OrderUtils
├── PaymentUtils
├── InventoryUtils
├── NotificationUtils
├── DatabaseUtils
└── EverythingUtils

bis jeder Dienst davon abhängt. Zu diesem Zeitpunkt hat man innerhalb eines Monorepos einen verteilten Monolith geschaffen: Eine Änderung an einer Hilfsfunktion erzwingt ein Neubauen, Neuüberprüfen und Neuveröffentlichen aller Komponenten. Die obersten Verzeichnisse sehen weiterhin ordentlich aus:

apps/
packages/

Die zugrunde liegende Architektur ist jedoch stark voneinander abhängig. Befolgen Sie diese strenge Regel: Teilen Sie Infrastruktur, Verträge sowie wirklich generische Hilfsfunktionen; behalten Sie die Domänenlogik innerhalb des Services, der für sie verantwortlich ist.

Ein schichtbasiertes Denkmodell

Es hilft, das System als Schichten vorzustellen – mit den Geschäftsdomänen oben, gemeinsamen Verträgen und Infrastruktur darunter sowie Monorepo-Tools am Grund:

              Business Domains
                       │
       ┌───────────────┼───────────────┐
       ↓               ↓               ↓
     Order          Payment         Inventory
       │               │               │
       └───────────────┼───────────────┘
                       ↓
                Shared Contracts
                       │
                       ↓
               Shared Infrastructure
                       │
                       ↓
                Monorepo Tooling
                       │
                       ↓
                  Turborepo

Turborepo befindet sich nahe am unteren Ende. Es sollte Ihre Architektur unterstützen, sie nicht beeinflussen.

Entscheidung darüber, es zu übernehmen

Gehen Sie nacheinander vier Fragen durch.

Haben Sie mehr als eine Anwendung?

Falls nicht, ist die Antwort eindeutig:

Keep it simple.

Falls ja, gehen Sie weiter.

Teilen sich die Anwendungen umfangreiche Tools oder Code?

Falls nicht:

Polyrepo may be perfectly reasonable.

Falls ja:

Monorepo becomes attractive.

Werden die Build-, Test- und Lint-Zeiten immer teurer?

Falls ja:

Turborepo's orchestration and caching become valuable.

Führen Sie Next.js, NestJS, Worker-Prozesse sowie gemeinsame TypeScript-Pakete zusammen aus?

Falls ja:

Turborepo becomes a strong candidate.

Prognostizierbarkeit ist der eigentliche Vorteil

Schnelligkeit ist ein Nebeneffekt. Der größere Wert liegt darin, dass ein großes Repository prognostizierbar wird. Wenn die Anwendungen zunehmen, muss jemand für jede Änderung antworten:

What changed?

und eine Reihe von Folgefragen (der Quellcode führt sie in einer Zeile aus): Was muss neu gebaut werden, was muss erneut getestet werden, was kann parallel ausgeführt werden, was kann aus dem Cache genutzt werden und auf welche Pakete wirkt sich die Änderung aus:

What needs to rebuild?What needs to be tested?What can run in parallel?What can come from cache?Which packages depend on this change?

Ohne Tooling beantworten Ingenieure diese Fragen manuell; mit einem Monorepo-Build-System wird der Großteil dieser Informationen aus dem Graphen abgeleitet.

Haupterkenntnisse

Turborepo behebt keine mangelhaften Servicegrenzen, schwaches Datenbankdesign, starke Kopplungen, schlechte API-Verträge, unklare Domainzuordnungen oder schlecht entworfene Microservices. Stattdessen sorgt es dafür, dass ein wachsendes TypeScript-Repository handhabbar bleibt. Die gesamte Architektur lässt sich in einem Überblick darstellen:

                Business Architecture
                         │
                         ↓
                    NestJS Apps
                         │
                         ↓
                  Shared Packages
                         │
                         ↓
                      pnpm
                         │
                         ↓
                    Turborepo
                         │
              ┌──────────┼──────────┐
              ↓          ↓          ↓
            Build      Test       Lint
              │          │          │
              └──────────┼──────────┘
                         ↓
                       CI/CD
  • Ein Monorepo entscheidet, wo der Code gespeichert wird; pnpm verwaltet den Arbeitsbereich; Turborepo orchestriert und speichert Aufgaben; NestJS baut die Services. Halten Sie diese Rollen getrennt.
  • Teiligen Sie sich Typen, Konfigurationen, Logging- und Messaging-Abstraktionen; behalten Sie die Domainlogik bei ihrem zuständigen Service, um einen verteilten Monolithen zu vermeiden.
  • Deklarieren Sie Aufgabenbeziehungen mit dependsOn sowie Ausgaben in turbo.json, damit das Caching sowohl effektiv als auch korrekt funktioniert.
  • Die Struktur des Repositories und die Bereitstellung sind unabhängig voneinander: Jeder Service kann weiterhin eigenständig bereitgestellt und skaliert werden.
  • Nehmen Sie Turborepo in Betracht, wenn die Koordination mehrerer TypeScript-Anwendungen teurer ist als die Einführung des Tools. Bei einer einzelnen Nest API ist das in der Regel nicht der Fall.