Exchange-Pipeline statt Monolith, und wann die schlankere Lösung reicht
urql ersetzt den monolithischen Client-Kern anderer GraphQL-Clients durch eine Exchange-Pipeline aus austauschbaren, einzeln testbaren Middleware-Funktionen. Normalisiertes Caching ist mit Graphcache optional statt Standard. Dieser Artikel zeigt, wann diese schlankere Architektur einem Projekt tatsächlich reicht.
Inhaltsverzeichnis
- 1. Warum ein dritter GraphQL-Client neben Apollo und Relay überhaupt existiert
- 2. Die Exchange-Pipeline-Architektur: urqls Grundprinzip
- 3. Die Standard-Exchanges: dedupExchange, cacheExchange, fetchExchange
- 4. Graphcache: optionales normalisiertes Caching als Exchange
- 5. Eigene Exchanges schreiben: Beispiel Auth-Exchange
- 6. Bundle-Größe und Einstiegshürde im Vergleich zu Apollo Client
- 7. Wann die geringere Feature-Tiefe von urql kein Nachteil ist
- 8. Wann Apollo Client trotz höherem Funktionsumfang die bessere Wahl bleibt
- 9. Entscheidungshilfe: urql, Apollo Client oder Relay
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein dritter GraphQL-Client neben Apollo und Relay überhaupt existiert
Apollo Client und Relay dominieren die Diskussion um GraphQL-Clients, bringen aber beide einen erheblichen Funktionsumfang mit, der nicht jedes Projekt braucht: ausgefeilte Cache-Policies, Optimistic-UI-Mechanismen, Compiler-generierte Artefakte oder ein tiefes Plugin-System. urql, entwickelt vom Team hinter Formidable und später von The Guild weitergeführt, entstand aus der Beobachtung, dass viele Projekte einen deutlich schlankeren Client mit klar nachvollziehbarem Verhalten bevorzugen.
Statt eines fest verdrahteten Kerns mit optionalen Erweiterungen setzt urql von Grund auf auf eine Pipeline aus einzelnen Funktionen, den Exchanges, durch die jede Operation nacheinander läuft. Das macht das Kernverhalten des Clients leicht nachvollziehbar und erlaubt es, einzelne Verhaltensweisen gezielt auszutauschen, ohne den gesamten Client zu verstehen oder zu forken.
2. Die Exchange-Pipeline-Architektur: urqls Grundprinzip
Ein Exchange in urql ist eine reine Funktion, die einen Stream eingehender Operationen entgegennimmt und einen Stream von Ergebnissen zurückgibt, ähnlich einer Middleware in Express oder Redux. Jede Anfrage durchläuft die konfigurierte Kette von Exchanges der Reihe nach, wobei jeder Exchange entscheiden kann, die Operation unverändert weiterzureichen, zu transformieren oder direkt selbst zu beantworten, etwa aus einem Cache, ohne dass die Anfrage das Netzwerk überhaupt erreicht.
Diese Architektur macht urql von Grund auf erweiterbar, ohne dass The Guild oder die Community jede denkbare Funktion in den Kern einbauen muss: Ein eigener Exchange für Authentifizierung, Retry-Logik oder Request-Deduplizierung lässt sich als isolierte, unabhängig testbare Funktion schreiben und einfach in die Kette einhängen, an genau der Position, an der er wirken soll.
import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql'
const client = createClient({
url: 'https://api.mironsoft.de/graphql',
exchanges: [dedupExchange, cacheExchange, fetchExchange],
})
3. Die Standard-Exchanges: dedupExchange, cacheExchange, fetchExchange
Ein Standard-urql-Client besteht typischerweise aus drei Exchanges in fester Reihenfolge: dedupExchange erkennt und bündelt identische, gleichzeitig laufende Anfragen zu einer einzigen Netzwerk-Operation, cacheExchange beantwortet Anfragen aus einem einfachen, dokumentbasierten Cache, wenn möglich, und fetchExchange führt am Ende der Kette den eigentlichen Netzwerk-Request aus, falls kein vorheriger Exchange die Anfrage bereits beantwortet hat.
Dieser Standard-Cache arbeitet dokumentbasiert statt normalisiert: Jede Query wird als Ganzes gecacht, ohne einzelne Entitäten typ- und ID-basiert zu verknüpfen, wie es Apollo Clients InMemoryCache oder Relays Store tun. Das reicht für viele Anwendungsfälle völlig aus und vermeidet die Komplexität der Cache-Invalidierung, verlangt aber, dass ein Team bewusst entscheidet, wann diese einfachere Cache-Strategie an ihre Grenzen stößt.
import { Client, dedupExchange, cacheExchange, fetchExchange } from 'urql'
// The default pipeline: dedupe -> document cache -> network fetch
const client = new Client({
url: '/graphql',
exchanges: [
dedupExchange,
cacheExchange,
fetchExchange,
],
})
4. Graphcache: optionales normalisiertes Caching als Exchange
Sobald ein Projekt echtes normalisiertes Caching braucht, etwa weil eine mutierte Entität in mehreren unabhängigen Queries konsistent aktualisiert werden soll, ersetzt Graphcache als eigener Exchange einfach den Standard-cacheExchange in der Kette. Graphcache normalisiert Objekte nach Typ und ID, ähnlich wie Apollo Clients InMemoryCache, bringt aber im Gegensatz dazu eine explizite Update-API, bei der Entwickler festlegen, wie eine Mutation den Cache anderer betroffener Queries verändert.
Weil Graphcache als austauschbarer Exchange implementiert ist statt als fest eingebauter Bestandteil, bleibt der Rest der urql-Konfiguration unverändert, wenn ein Team von dokumentbasiertem zu normalisiertem Caching wechselt. Das erlaubt es, mit dem einfachen Standard-Cache zu starten und erst dann auf Graphcache zu wechseln, wenn ein konkretes Konsistenzproblem tatsächlich auftritt, statt die Komplexität von Anfang an vorwegzunehmen.
import { createClient, fetchExchange } from 'urql'
import { cacheExchange } from '@urql/exchange-graphcache'
const cache = cacheExchange({
keys: {
Product: (product) => product.id,
},
updates: {
Mutation: {
addReview: (result, args, cache) => {
cache.invalidate({ __typename: 'Product', id: args.productId })
},
},
},
})
const client = createClient({
url: '/graphql',
exchanges: [cache, fetchExchange],
})
5. Eigene Exchanges schreiben: Beispiel Auth-Exchange
Weil jeder Exchange eine reine, unabhängig testbare Funktion ist, lässt sich Cross-Cutting-Logik wie Authentifizierung sauber isolieren, statt sie über Request-Interceptors oder globale Konfiguration zu verteilen. Ein Auth-Exchange fügt beispielsweise bei jeder ausgehenden Operation einen aktuellen Access-Token als Header hinzu und kann bei einer 401-Antwort automatisch einen Token-Refresh anstoßen und die ursprüngliche Operation danach erneut durch die Pipeline schicken.
Das offizielle Paket @urql/exchange-auth implementiert genau dieses Muster bereits vorgefertigt, zeigt aber auch, wie leicht sich ein eigener, projektspezifischer Exchange schreiben lässt, wenn die Standardlösung nicht exakt passt. Diese Erweiterbarkeit ohne Forken des Kerns ist einer der praktischen Vorteile der Exchange-Architektur gegenüber monolithischeren Clients.
import { authExchange } from '@urql/exchange-auth'
const auth = authExchange(async (utils) => {
let token = localStorage.getItem('authToken')
return {
addAuthToOperation: (operation) => {
return utils.appendHeaders(operation, {
Authorization: token ? `Bearer ${token}` : '',
})
},
didAuthError: (error) => error.response?.status === 401,
refreshAuth: async () => {
token = await refreshAccessToken()
localStorage.setItem('authToken', token)
},
}
})
6. Bundle-Größe und Einstiegshürde im Vergleich zu Apollo Client
Der urql-Kern ohne Graphcache bringt nur einen Bruchteil der Bundle-Größe von Apollo Client mit, weil Funktionen wie normalisiertes Caching, Optimistic Updates oder Local-State-Management nicht standardmäßig enthalten sind, sondern nur bei Bedarf über zusätzliche Exchanges hinzukommen. Für Projekte, bei denen Bundle-Größe auf mobilen Endgeräten oder in Low-Bandwidth-Umgebungen eine reale Rolle spielt, ist dieser Unterschied spürbar.
Die Einstiegshürde ist entsprechend niedriger: Ein Grundsetup mit useQuery und useMutation lässt sich in wenigen Zeilen aufsetzen, ohne dass ein Team sich zuerst durch Cache-Policies, Type-Policies oder Fragment-Konventionen arbeiten muss, wie es bei Apollo Client für fortgeschrittene Anwendungsfälle nötig wird.
7. Wann die geringere Feature-Tiefe von urql kein Nachteil ist
Für kleine bis mittelgroße Anwendungen, deren Datenmodell überschaubar bleibt und bei denen Mutationen selten mehrere unabhängige Queries gleichzeitig betreffen, reicht der einfache dokumentbasierte Standard-Cache von urql meist völlig aus. Auch Projekte, die ohnehin server-seitiges Rendering mit häufigem Neuladen ganzer Seiten nutzen, profitieren selten von den ausgefeilten Optimistic-UI-Mechanismen, die Apollo Client oder Relay mitbringen.
Ebenso passt urql gut zu Teams, die bewusst eine kleinere, leichter vollständig verstehbare Codebasis bevorzugen, etwa in Bibliotheken oder Widgets, die in fremde Anwendungen eingebettet werden, wo jedes zusätzliche Kilobyte Bundle-Größe zählt. In solchen Kontexten wiegt die geringere Feature-Tiefe von urql weniger schwer als der Vorteil einer kleineren, klar nachvollziehbaren Abhängigkeit.
8. Wann Apollo Client trotz höherem Funktionsumfang die bessere Wahl bleibt
Sobald ein Projekt komplexe, feingranulare Cache-Policies pro Feldtyp braucht, etwa unterschiedliches Verhalten für paginierte Listen gegenüber Einzelobjekten, oder auf ein großes Ökosystem an fertigen Integrationen wie Apollo Studio, Apollo Router und offizieller Federation-Unterstützung angewiesen ist, bietet Apollo Client mehr eingebaute Lösungen, ohne dass ein Team diese selbst über Exchanges nachbauen muss.
Auch die schiere Größe der Community spricht in komplexen Projekten für Apollo Client: Für nahezu jedes fortgeschrittene Caching- oder Pagination-Problem existiert bereits eine dokumentierte Lösung oder ein offizielles Paket, während urql-Teams bei sehr spezifischen Anforderungen häufiger eigene Exchanges von Grund auf entwickeln müssen.
9. Entscheidungshilfe: urql, Apollo Client oder Relay
Wer eine schlanke, leicht verständliche Grundlage mit optionalem normalisiertem Caching sucht und keine tiefe Anbindung an ein bestimmtes Ökosystem braucht, ist mit urql gut bedient. Wer feingranulare Cache-Kontrolle, ein großes Plugin-Ökosystem oder offizielle Federation-Unterstützung braucht, findet das eher bei Apollo Client, und wer maximale Konsistenz in einem sehr großen React-Team erzwingen will, bei Relay.
Die folgende Tabelle stellt die drei Clients entlang der wichtigsten Kriterien gegenüber.
| Kriterium | urql | Apollo Client | Relay |
|---|---|---|---|
| Architektur | Exchange-Pipeline | Monolithischer Kern mit Link-Chain | Compiler-generierter Store |
| Normalisiertes Caching | Optional über Graphcache | Standard über InMemoryCache | Standard, strikt typisiert |
| Bundle-Größe | Sehr klein im Grundsetup | Mittel bis groß | Mittel plus Compiler-Tooling |
| Einstiegshürde | Niedrig | Mittel | Hoch |
Mironsoft
GraphQL-Schema-Design, Resolver-Performance und API-Sicherheit
GraphQL-APIs, die unter echter Last stabil bleiben?
Wir prüfen bestehende GraphQL-Schemas und Resolver, decken N+1-Probleme und fehlende Query-Limits auf und bauen daraus eine API, die Performance, Sicherheit und Wartbarkeit gleichzeitig hält.
Schema-Review
Typen, Resolver und Berechtigungen auf Konsistenz und Sicherheitslücken prüfen.
Performance-Optimierung
DataLoader, Caching und Query-Complexity-Limits gegen N+1 und Overfetching einsetzen.
Produktions-Absicherung
Rate-Limiting, Introspection-Schutz und Monitoring für den produktiven Betrieb einrichten.
10. Zusammenfassung
urql: Das Wichtigste auf einen Blick
Architektur
urql ersetzt einen monolithischen Client-Kern durch eine Kette austauschbarer Exchanges, die jede Operation der Reihe nach verarbeiten.
Caching
Der Standard-Cache arbeitet dokumentbasiert, Graphcache liefert bei Bedarf normalisiertes Caching als austauschbaren Exchange.
Bundle-Größe
Ohne Graphcache bringt urql nur einen Bruchteil der Bundle-Größe von Apollo Client mit, relevant für mobile und eingebettete Anwendungen.
Passender Einsatz
Kleine bis mittlere Apps mit überschaubarem Datenmodell profitieren am meisten, komplexe Cache-Policies sprechen eher für Apollo Client.