urql als leichtgewichtiger GraphQL-Client: wann er reicht
AI generated
{ }
type
GraphQL · Client · React · Tooling
urql als leichtgewichtiger GraphQL-Client
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.

15 Min. Lesezeit Exchange-Pipeline · Graphcache React · Vue · Svelte

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.

11. FAQ: urql: Das Wichtigste auf einen Blick

1Was unterscheidet urql architektonisch von Apollo Client?
urql verarbeitet jede Operation durch eine Kette austauschbarer Exchange-Funktionen, während Apollo Client einen fest verdrahteten Kern mit einer Link-Chain für Netzwerklogik und einem eingebauten InMemoryCache nutzt.
2Ist normalisiertes Caching in urql standardmäßig aktiv?
Nein. Der Standard-cacheExchange arbeitet dokumentbasiert. Normalisiertes Caching kommt erst hinzu, wenn Graphcache als eigener Exchange in die Kette eingehängt wird.
3Was ist Graphcache?
Graphcache ist ein optionaler Exchange für urql, der Objekte nach Typ und ID normalisiert cacht, ähnlich wie Apollo Clients InMemoryCache, inklusive expliziter Update-API für Mutationen.
4Ist urql für React Native geeignet?
Ja, urql funktioniert framework-übergreifend und wird offiziell für React, Vue, Svelte und React Native unterstützt, jeweils mit eigenen Bindings für die Exchange-Pipeline.
5Wie schreibe ich einen eigenen Exchange?
Ein Exchange ist eine reine Funktion, die einen Operationen-Stream entgegennimmt und einen Ergebnis-Stream zurückgibt. Für Standardfälle wie Authentifizierung existieren bereits offizielle Pakete wie @urql/exchange-auth als Vorlage.
6Wann reicht der Standard-Cache von urql nicht mehr aus?
Sobald eine Mutation eine Entität ändert, die in mehreren unabhängigen, bereits gecachten Queries auftaucht und dort konsistent aktualisiert werden muss, ist normalisiertes Caching über Graphcache meist die bessere Wahl.
7Unterstützt urql Federation?
urql selbst ist ein reiner Client und arbeitet unabhängig davon, ob das Backend föderiert ist oder nicht, solange es ein einziges GraphQL-Schema anspricht, wie es ein Federation-Router nach außen bereitstellt.
8Wie groß ist der Bundle-Size-Unterschied zwischen urql und Apollo Client wirklich?
Der urql-Kern ohne Graphcache liegt im niedrigen einstelligen Kilobyte-Bereich, während Apollo Client mit InMemoryCache und Standard-Setup deutlich mehr Kilobyte in das Bundle bringt. Für die meisten Web-Apps ist der Unterschied spürbar, aber selten entscheidend.
9Kann ich später von urql zu Apollo Client wechseln?
Technisch ja, aber Queries, Cache-Konfiguration und teils die Komponentenstruktur müssen angepasst werden, da beide Clients unterschiedliche Hooks und Cache-Modelle verwenden. Ein Wechsel ist eher ein Rewrite als eine einfache Migration.
10Für welche Art von Projekt ist urql die klare Empfehlung?
Für kleine bis mittelgroße Anwendungen mit überschaubarem Datenmodell, für Bibliotheken und Widgets, bei denen Bundle-Größe zählt, und für Teams, die eine kleine, vollständig nachvollziehbare Client-Codebasis bevorzugen.