React Monorepo mit Turborepo aufbauen
AI generated
</>
{ }
React · Monorepo · Turborepo · CI/CD
React Monorepo mit Turborepo aufbauen
von der Workspace-Struktur bis zum Remote Cache

Ein React Monorepo mit Turborepo bündelt mehrere Apps und geteilte Pakete in einem Repository, ohne dass jeder Build alles neu kompilieren muss. Dieser Artikel zeigt die Workspace-Struktur, die turbo.json-Konfiguration, Remote Caching und eine CI-Pipeline, die nur tatsächlich betroffene Pakete baut.

18 Min. Lesezeit Turborepo · npm Workspaces · Changesets Remote Caching · Selektive Builds

1. Warum ein Monorepo für mehrere React-Apps sinnvoll ist

Sobald ein Unternehmen mehr als eine React-Anwendung betreibt, etwa einen Shop-Frontend, ein Admin-Dashboard und eine Marketing-Seite, stellt sich zwangsläufig die Frage nach geteiltem Code. Ein React Monorepo bündelt alle diese Anwendungen zusammen mit gemeinsam genutzten Paketen wie einer UI-Komponenten-Bibliothek oder einem API-Client in einem einzigen Repository, statt sie über mehrere separate Git-Repositories und veröffentlichte npm-Pakete zu verteilen.

Der offensichtlichste Vorteil eines React Monorepo liegt in atomaren Commits über Grenzen hinweg: eine Änderung an einer geteilten Komponente und die Anpassung aller Verbraucher dieser Komponente landen im selben Commit, statt über mehrere Repositories und Versions-Updates synchronisiert werden zu müssen. Der Nachteil ohne geeignetes Tooling ist jedoch, dass naive Build-Skripte bei jeder Änderung das gesamte Repository neu bauen, selbst wenn nur eine einzelne App betroffen ist.

Turborepo löst genau dieses Problem: es versteht die Abhängigkeiten zwischen den Paketen eines React Monorepo, cached Build-Ergebnisse aggressiv und baut nur, was sich tatsächlich geändert hat. Die folgenden Abschnitte zeigen den kompletten Aufbau von der Grundstruktur bis zur produktionsreifen CI-Pipeline.

2. Turborepo-Grundstruktur: apps, packages und Workspaces

Ein React Monorepo mit Turborepo folgt üblicherweise einer Zwei-Ordner-Konvention: apps/ enthält die eigenständig deploybaren Anwendungen, packages/ enthält geteilten Code, der von mehreren Apps importiert wird, aber selbst nicht direkt deployt wird. Diese Trennung macht auf einen Blick klar, was ein Endprodukt ist und was reine Infrastruktur für andere Pakete darstellt.


my-monorepo/
├── apps/
│   ├── shop-frontend/        # Vite + React, deployed to Vercel
│   │   ├── package.json
│   │   └── src/
│   └── admin-dashboard/      # Next.js, deployed separately
│       ├── package.json
│       └── src/
├── packages/
│   ├── ui/                   # Shared React component library
│   │   ├── package.json
│   │   └── src/
│   ├── api-client/           # Typed fetch wrapper, shared across apps
│   │   ├── package.json
│   │   └── src/
│   └── tsconfig/             # Shared tsconfig.json base files
│       └── base.json
├── turbo.json
├── package.json               # root workspace manifest
└── package-lock.json

Die Verknüpfung zwischen den Paketen läuft über npm, Yarn oder pnpm Workspaces, die im Root package.json deklariert werden. Ein Paket wie @repo/ui wird in apps/shop-frontend/package.json ganz normal als Abhängigkeit eingetragen, zeigt aber über einen Workspace-Link auf den lokalen Ordner, statt auf die npm-Registry. Änderungen an packages/ui stehen dadurch sofort in allen Apps zur Verfügung, ohne Publish-Schritt für ein React Monorepo in der lokalen Entwicklung.

3. turbo.json: Pipeline, Caching und Task-Abhängigkeiten

Das Herzstück jeder Turborepo-Konfiguration für ein React Monorepo ist die Datei turbo.json. Sie definiert, welche Tasks wie build, test oder lint existieren, in welcher Reihenfolge sie über Paketgrenzen hinweg ausgeführt werden müssen und welche Ausgaben gecached werden dürfen.


{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "inputs": ["src/**", "package.json", "tsconfig.json"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Das Präfix ^ vor build in dependsOn bedeutet, dass zuerst alle Pakete gebaut werden müssen, von denen das aktuelle Paket abhängt, bevor es selbst gebaut wird. Für ein React Monorepo stellt das sicher, dass apps/shop-frontend niemals gegen eine veraltete Version von packages/ui baut. Der Schlüssel outputs teilt Turborepo mit, welche Verzeichnisse als Build-Ergebnis gecached werden sollen, während inputs bestimmt, welche Dateien einen Cache-Miss auslösen, wenn sie sich ändern.

4. Remote Caching: Build-Zeiten im Team drastisch senken

Lokales Caching in Turborepo speichert Build-Ergebnisse bereits auf der eigenen Festplatte, sodass ein unveränderter Task beim zweiten Aufruf sofort aus dem Cache beantwortet wird. Der eigentliche Mehrwert für ein Team entsteht aber erst mit Remote Caching, das denselben Cache über Vercel Remote Cache oder eine selbst gehostete Alternative mit dem gesamten Team und der CI-Pipeline teilt.


# Authenticate once against the Turborepo Remote Cache
npx turbo login
npx turbo link

# From now on, a build already run by a teammate or CI
# is fetched from the remote cache instead of rebuilt locally
npx turbo run build
# Cache hit, replaying output for packages/ui, apps/shop-frontend

Der praktische Effekt für ein React Monorepo im Team: wenn eine Kollegin bereits denselben Commit gebaut und getestet hat, lädt jeder andere Entwickler und jeder CI-Runner das Ergebnis in Sekunden statt in Minuten aus dem Remote Cache herunter. Bei größeren Monorepos mit vielen Paketen summiert sich dieser Effekt zu einer spürbaren Reduktion der gesamten CI-Laufzeit, oft um 60 bis 80 Prozent gegenüber einem nicht gecachten Build.

5. Geteilte UI-Komponenten-Pakete zwischen mehreren Apps

Ein häufiger Anwendungsfall für ein React Monorepo ist eine geteilte UI-Komponenten-Bibliothek, die von allen Apps im Repository genutzt wird, ohne über die öffentliche npm-Registry veröffentlicht zu werden. Das Paket packages/ui exportiert dafür seine Komponenten über ein package.json mit korrektem exports-Feld und wird von den konsumierenden Apps als normale Abhängigkeit importiert.

Wichtig ist dabei, dass packages/ui selbst keine eigene Build-Pipeline zwingend braucht, wenn das konsumierende Framework, etwa Vite oder Next.js, TypeScript-Quellcode direkt transpilieren kann. Für strengere Setups mit eigenständigem Build lohnt sich tsup, ein schneller Bundler speziell für TypeScript-Bibliotheken, der ESM- und CommonJS-Ausgaben gleichzeitig erzeugt. In beiden Fällen bleibt der entscheidende Vorteil des React Monorepo erhalten: eine Änderung an einer Button-Komponente ist sofort in allen Apps sichtbar, ohne einen Versions-Bump und Publish-Zyklus abzuwarten.

6. Selektive Builds mit --filter: nur betroffene Pakete bauen

Bei einem großen React Monorepo mit einem Dutzend Apps wäre es Verschwendung, bei jeder Pull-Request-Prüfung wirklich alle Pakete zu bauen und zu testen, wenn sich nur eine einzige App geändert hat. Turborepos --filter-Flag erlaubt es, gezielt nur die betroffenen Pakete und ihre Abhängigkeiten anzusprechen.


# Build only shop-frontend and everything it depends on
npx turbo run build --filter=shop-frontend...

# Build only packages that changed since the main branch (used in CI)
npx turbo run build --filter="...[origin/main]"

# Combine: only affected packages, but include their dependents too
npx turbo run test --filter="...[origin/main]" --filter="...^shop-frontend"

Die Syntax ...[origin/main] ist besonders wertvoll für ein React Monorepo in der CI, weil sie Turborepo anweist, per Git-Diff herauszufinden, welche Pakete sich seit dem Hauptbranch geändert haben, und automatisch auch alle davon abhängigen Pakete einzuschließen. Eine Änderung an packages/ui löst dadurch korrekt einen Rebuild aller Apps aus, die diese Komponentenbibliothek nutzen, während eine isolierte Änderung an einer einzelnen App keine unnötigen Builds anderer, unbeteiligter Apps auslöst.

7. Versionierung und Publishing mit Changesets

Sobald ein React Monorepo Pakete enthält, die tatsächlich als npm-Pakete veröffentlicht werden sollen, etwa eine interne Design-System-Bibliothek für mehrere Teams, wird Versionierung zur eigenen Herausforderung. Changesets löst das, indem jede Pull Request, die ein veröffentlichbares Paket ändert, eine kleine Markdown-Datei im Ordner .changeset/ hinzufügt, die beschreibt, welches Paket wie versioniert werden soll.

Ein GitHub-Actions-Workflow sammelt diese Changeset-Dateien beim Merge in den Hauptbranch, aktualisiert automatisch die package.json-Versionsnummern nach Semantic Versioning und erzeugt einen CHANGELOG-Eintrag. Für ein React Monorepo mit mehreren unabhängig versionierten Paketen vermeidet dieser Prozess manuelle Versions-Bumps und stellt sicher, dass jede Änderung nachvollziehbar dokumentiert ist, bevor sie tatsächlich veröffentlicht wird.

8. CI-Pipeline für ein Turborepo-Monorepo

Eine produktionsreife CI-Pipeline für ein React Monorepo kombiniert Remote Caching mit selektiven Filtern, um sowohl Korrektheit als auch Geschwindigkeit sicherzustellen. Der entscheidende Trick: fetch-depth: 0 beim Checkout, damit Turborepo den vollständigen Git-Verlauf für den Vergleich mit dem Hauptbranch zur Verfügung hat.


# .github/workflows/ci.yml
name: Monorepo CI

on:
  pull_request:
    branches: [main]

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Turborepo needs full history for --filter=[origin/main]

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci

      - name: Lint, test and build only affected packages
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}
        run: |
          npx turbo run lint test build --filter="...[origin/main]"

Die Umgebungsvariablen TURBO_TOKEN und TURBO_TEAM verbinden die CI-Pipeline mit dem Remote Cache, sodass ein bereits lokal getesteter Commit in der CI nicht erneut vollständig gebaut werden muss. Für ein React Monorepo mit vielen Paketen reduziert diese Kombination aus selektivem Filtering und Remote Caching die durchschnittliche CI-Laufzeit oft von zehn Minuten auf unter zwei, besonders bei kleinen, fokussierten Pull Requests.

9. Monorepo gegen Multi-Repo im Vergleich

Die Entscheidung zwischen einem React Monorepo und mehreren getrennten Repositories hängt von Teamgröße, Anzahl geteilter Pakete und Release-Kadenz ab. Die folgende Tabelle vergleicht die wichtigsten Unterschiede.

Kriterium React Monorepo (Turborepo) Multi-Repo
Geteilter Code Direkt über Workspaces, kein Publish nötig Muss veröffentlicht und versioniert werden
Atomare Änderungen Ein Commit über Paketgrenzen hinweg Mehrere PRs und Versions-Updates nötig
CI-Laufzeit ohne Caching Kann bei naivem Setup lang werden Naturgemäß auf ein Repo begrenzt
CI-Laufzeit mit Turborepo Selektiv, oft schneller als Multi-Repo Kein Vergleich nötig, pro Repo isoliert
Zugriffskontrolle pro Team Feingranular schwieriger Repo-Grenzen sind natürliche Teamgrenzen

Für Teams mit mehreren zusammenhängenden React-Apps und viel geteiltem Code ist ein React Monorepo mit Turborepo fast immer die effizientere Wahl, weil es geteilten Code ohne Publish-Overhead ermöglicht und mit Remote Caching sogar bei wachsender Codebasis performant bleibt. Multi-Repo bleibt sinnvoll, wenn Teams komplett unabhängig arbeiten und kaum Code teilen, oder wenn strikte Zugriffstrennung zwischen Teams eine harte Anforderung ist.

Mironsoft

Monorepo-Architektur und CI/CD-Optimierung für React-Teams

Mehrere React-Apps in einem effizienten Monorepo bündeln?

Wir migrieren bestehende Multi-Repo-Setups in ein Turborepo-Monorepo, richten Remote Caching ein und bauen eine selektive CI-Pipeline, die nur tatsächlich betroffene Pakete baut.

Monorepo-Migration

Bestehende Multi-Repo-Struktur in ein Turborepo-Setup überführen

Remote Caching

Team- und CI-weites Caching für drastisch kürzere Build-Zeiten einrichten

CI-Pipeline

Selektive Builds mit --filter und Changesets für sauberes Versionsmanagement

10. Zusammenfassung

Ein React Monorepo mit Turborepo löst das Grundproblem geteilten Codes zwischen mehreren Anwendungen, ohne die Nachteile naiver Full-Rebuild-Skripte in Kauf zu nehmen. Die Kombination aus apps/- und packages/-Struktur, einer klar definierten turbo.json-Pipeline und Remote Caching macht Build-Zeiten für das gesamte Team und die CI-Pipeline planbar und kurz.

Selektive Builds mit --filter stellen sicher, dass eine kleine Änderung an einer einzelnen App keinen unnötigen Rebuild des gesamten React Monorepo auslöst, während Changesets die Versionierung veröffentlichter Pakete strukturiert dokumentieren. Für Teams mit mehreren zusammenhängenden React-Apps ist dieser Ansatz mittlerweile der Industriestandard, weil er geteilten Code ohne Publish-Overhead ermöglicht und mit wachsender Codebasis performant bleibt.

React Monorepo mit Turborepo — Das Wichtigste auf einen Blick

Workspace-Struktur

apps/ für deploybare Anwendungen, packages/ für geteilten Code, verknüpft über npm, Yarn oder pnpm Workspaces.

turbo.json Pipeline

dependsOn mit ^-Präfix erzwingt korrekte Build-Reihenfolge, outputs definiert cachebare Ergebnisse.

Remote Caching

Team und CI teilen sich denselben Cache, reduziert Build-Zeiten oft um 60 bis 80 Prozent.

Selektive Builds

--filter="...[origin/main]" baut nur tatsächlich betroffene Pakete und deren Abhängige.

11. FAQ: React Monorepo mit Turborepo

1Wann lohnt sich ein React Monorepo?
Bei mehreren Apps mit viel geteiltem Code vereinfacht ein Monorepo atomare Änderungen deutlich.
2Was macht das ^-Präfix in dependsOn?
Erzwingt, dass abhängige Pakete zuerst gebaut werden, damit nie gegen veraltete Versionen gebaut wird.
3Was ist Remote Caching?
Teilt Build-Ergebnisse zwischen Team und CI, sodass bereits gebaute Commits nicht erneut kompiliert werden.
4Wie funktioniert --filter?
Ermittelt per Git-Diff geänderte Pakete und baut automatisch auch alle davon abhängigen mit.
5Muss ein geteiltes Paket veröffentlicht werden?
Nein, innerhalb desselben Monorepos reicht ein Workspace-Link ohne npm-Publish.
6Wofür sind Changesets da?
Dokumentieren Versionsänderungen pro Pull Request und automatisieren Versions-Bumps beim Merge.
7Warum fetch-depth: 0 in CI?
Turborepo braucht vollständigen Git-Verlauf, um Änderungen gegenüber dem Hauptbranch zu erkennen.
8Wie stark sinkt die CI-Laufzeit?
Häufig um 60 bis 80 Prozent, besonders bei kleinen, fokussierten Pull Requests.
9Ist Turborepo mit pnpm kompatibel?
Ja, es funktioniert gleichermaßen mit npm, Yarn und pnpm Workspaces.
10Wann ist Multi-Repo vorzuziehen?
Bei komplett unabhängigen Teams mit wenig geteiltem Code oder strikter Zugriffstrennung.