23. Februar 2026
Typsichere API-Codegenerierung für React in 2026
Das Ökosystem hat sich gefunden. Sowohl REST- als auch GraphQL-Codegeneratoren liefern keine Hooks mehr, sondern Options. Hier ist das vollständige Bild.
Sascha Becker
Author18 Min. Lesezeit

Typsichere API-Codegenerierung für React in 2026
Jedes Jahr stelle ich mir dieselbe Frage: Was ist aktuell der beste Weg, typsicheren und ergonomischen Frontend-Code aus meinen API-Definitionen zu generieren? Die Antwort ändert sich ständig. Das hier ist die 2026er-Ausgabe.
Die Kurzversion: Das Ökosystem hat konvergiert. Sowohl die REST- als auch die GraphQL-Welt sind unabhängig voneinander zum selben Schluss gekommen: keine framework-spezifischen Hooks mehr generieren, stattdessen framework-agnostische Options und typisierte Dokumente.
Wenn du bisher generierte useGetPet()- oder useFilmsQuery()-Hooks verwendet hast, wird es Zeit zu verstehen, warum dieses Pattern ausläuft und was es ersetzt hat.
Warum der Wechsel weg von generierten Hooks?
Das ist die wichtigste Veränderung in beiden Ökosystemen, also klären wir das direkt.
Früher haben Codegeneratoren für jeden Endpoint oder jede Query einen eigenen React-Hook produziert. Ein REST-Generator lieferte useGetPetById(), ein GraphQL-Generator useFilmsQuery(). Komfortabel? Absolut. Nachhaltig? Nein.
Die Probleme haben sich aufgestapelt:
Kombinatorischer Wartungsaufwand. Jede Kombination aus HTTP-Client (Axios, Fetch, Ky) × Data-Fetching-Library (TanStack Query, SWR, Apollo, urql) × Framework (React, Vue, Svelte, Solid, Angular) brauchte ein eigenes Plugin. Das GraphQL-Code-Generator-Team hat Dutzende davon gepflegt, jedes mit eigenen Konfigurationsmacken und Inkonsistenzen.
Framework-Lock-in im generierten Output. Generierte Hooks koppeln deine API-Schicht an React. Wenn dein Team auch eine Vue-App pflegt oder zu Solid migriert, bricht die gesamte Codegen-Pipeline zusammen.
Kompositionsprobleme. Reacts Rules of Hooks bedeuten, dass man einen generierten Hook nicht in einer Schleife oder bedingt aufrufen kann. Man kann sie auch nicht einfach mit useQueries() verwenden, das ein Array von Option-Objekten erwartet, keine Hook-Aufrufe.
Unnötige Abstraktionsschicht. Ein generierter Hook ist nur ein dünner Wrapper um useQuery({ queryKey, queryFn }). Seit TanStack Query v5 den queryOptions()-Helper eingeführt hat, ist dieser Wrapper überflüssiger Overhead.
Das GraphQL-Code-Generator-Team hat diesen Wechsel ausführlich in der v3/v5-Roadmap und der Client-Preset-Diskussion besprochen. Das Hey-API-Team hat sein TanStack-Query-Plugin von Anfang an nach dieser Philosophie gebaut (Issue #653).
Der Kleber: TanStack Query v5
Der Wechsel von Hooks zu Options wurde durch TanStack Query v5 ermöglicht, das den queryOptions()-Helper eingeführt hat:
tsimport { queryOptions } from "@tanstack/react-query";const todosOptions = queryOptions({queryKey: ["todos"],queryFn: fetchTodos,staleTime: 5000,});// In einem Hook verwendenconst { data } = useQuery(todosOptions);// Zum Prefetching verwendenawait queryClient.prefetchQuery(todosOptions);// Aus dem Cache lesen (voll typisiert)const cached = queryClient.getQueryData(todosOptions.queryKey);
Das ist nicht nur eine Convenience-Funktion. Es ist eine typsichere Options-Factory, die sicherstellt, dass queryKey, queryFn, Rückgabetypen und Cache-Typen synchron bleiben. Es ist das Primitiv, das Codegeneratoren jetzt ansteuern, anstatt eigene Hooks zu generieren.
Dasselbe Pattern existiert für Mutations (mutationOptions()) und Infinite Queries (infiniteQueryOptions()).
Die REST-Seite: OpenAPI-Codegenerierung
@hey-api/openapi-ts
Hey API ist der aktuelle Spitzenreiter für OpenAPI-zu-TypeScript-Generierung. Es ist der Nachfolger von openapi-typescript-codegen, komplett mit Plugin-basierter Architektur neu geschrieben.
Was es generiert:
- Typsichere SDK-Funktionen für jeden Endpoint
queryOptions/mutationOptions/infiniteQueryOptions-Funktionen für TanStack Query- Query-Key-Funktionen für Cache-Management
- Optionale Zod- oder Valibot-Validierungsschemas
Was es nicht generiert: Hooks.
Konfiguration
openapi-ts.config.tsimport { defineConfig } from "@hey-api/openapi-ts";export default defineConfig({input: "https://api.example.com/openapi.json",output: "src/client",plugins: ["@hey-api/typescript", "@hey-api/sdk", "@tanstack/react-query"],});
Ausführen:
bashnpx @hey-api/openapi-ts
Generierter Output
Der Generator produziert Options-Funktionen, einfache Funktionen, die Objekte zurückgeben:
ts// Generiert: src/client/@tanstack/react-query.gen.tsexport const getPetByIdOptions = (options: { path: { petId: number } }) => ({queryKey: getPetByIdQueryKey(options),queryFn: () => getPetById(options),});export const addPetMutation = () => ({mutationFn: (options: { body: { name: string } }) => addPet(options),});
Verwendung in Komponenten
Du spreadest die generierten Options in die Hooks von TanStack Query:
tsximport { useQuery, useMutation } from "@tanstack/react-query";import {getPetByIdOptions,addPetMutation,} from "./client/@tanstack/react-query.gen";function PetDetail({ petId }: { petId: number }) {const { data } = useQuery({...getPetByIdOptions({ path: { petId } }),staleTime: 5000, // beliebige zusätzliche Options});const mutation = useMutation({...addPetMutation(),onSuccess: () => {// Invalidieren, Weiterleiten, was auch immer nötig ist},});return <div>{data?.name}</div>;}
Dieses Pattern hat einen subtilen aber mächtigen Vorteil: Du kannst dieselben Options mit queryClient.prefetchQuery() für SSR, queryClient.getQueryData() für Cache-Reads und useQueries() für parallele Fetches verwenden, alles voll typisiert.
ts// Prefetch auf dem Server (Next.js)await queryClient.prefetchQuery(getPetByIdOptions({ path: { petId } }));// Aus dem Cache lesen (Rückgabetyp wird inferiert)const cached = queryClient.getQueryData(getPetByIdQueryKey({ path: { petId } }),);
HTTP-Clients
Hey API unterstützt Fetch (Standard), Axios, Ky und framework-spezifische Clients wie Next.js und Nuxt. Das TanStack-Query-Plugin funktioniert mit React, Vue, Svelte, Solid und Angular, alles aus demselben generierten Output.
Orval
Orval ist der andere große Player. Im Gegensatz zu Hey API generiert Orval standardmäßig immer noch eigene Hooks. Einen useListPets()-Hook, einen useGetPetById()-Hook und so weiter.
orval.config.tsimport { defineConfig } from "orval";export default defineConfig({petstore: {input: "./petstore.yaml",output: {target: "./src/api/endpoints.ts",client: "react-query",mock: true, // generiert MSW-Handler mit Faker.js},},});
Orvals großer Differenzierungspunkt ist die eingebaute Mock-Generierung. Es produziert MSW-Request-Handler mit realistischen Fake-Daten out of the box, was für die Frontend-Entwicklung gegen noch nicht existierende APIs hervorragend ist.
Es gibt einen offenen Feature Request für queryOptions-basierten Output. Wenn du ein neues Projekt startest, würde ich Hey API für den Options-basierten Ansatz empfehlen. Wenn du Orval bereits nutzt und Mock-Generierung brauchst, ist es immer noch eine solide Wahl.
Direktvergleich
| @hey-api/openapi-ts | Orval | |
|---|---|---|
| Output | Options-Objekte | Eigene Hooks |
| TanStack Query | React, Vue, Svelte, Solid, Angular | React, Vue, Svelte, Solid |
| Mock-Generierung | Separates Plugin | Eingebaut (MSW + Faker.js) |
| Validierung | Zod, Valibot | Zod |
| HTTP-Clients | Fetch, Axios, Ky, Next.js, Nuxt | Axios, Fetch |
| Reife | Pre-1.0, schnelle Entwicklung | v8, stabil |
Die GraphQL-Seite
graphql-codegen mit dem Client Preset
GraphQL Code Generator von The Guild ist das etablierte Tool. Der empfohlene Ansatz ist das Client Preset, das typisierte Document-Objekte generiert, keine Hooks.
Was sich geändert hat
Früher hat man @graphql-codegen/typescript-react-apollo oder @graphql-codegen/typescript-react-query installiert und generierte Hooks bekommen. Diese Plugins sind jetzt deprecated und in Community-Repos ausgelagert. Die offizielle Empfehlung ist:
- Das Client Preset nutzen, um eine typisierte
graphql()-Funktion zu generieren - Queries inline mit dieser Funktion schreiben
- Die typisierten Dokumente an die Hooks der eigenen Client-Library übergeben
Konfiguration
Die Konfiguration hängt davon ab, welchen GraphQL-Client du verwendest. Der entscheidende Unterschied ist documentMode:
- Standard (kein
documentMode) generiertTypedDocumentNode-Objekte (AST). Apollo Client und urql verstehen diese nativ. documentMode: "string"generiertTypedDocumentString-Werte (einfache Strings mit Typ-Metadaten). Das ist leichtgewichtiger, funktioniert aber nur mit Clients, die Strings akzeptieren, wie ein eigenerfetch-Wrapper für TanStack Query.
Für Apollo Client oder urql (generiert TypedDocumentNode):
codegen.tsimport type { CodegenConfig } from "@graphql-codegen/cli";const config: CodegenConfig = {schema: "https://api.example.com/graphql",documents: ["src/**/*.{ts,tsx}"],ignoreNoDocuments: true,generates: {"./src/gql/": {preset: "client",config: {enumsAsTypes: true,},presetConfig: {fragmentMasking: true,},},},};export default config;
Für TanStack Query mit eigenem Fetch-Wrapper (generiert TypedDocumentString):
codegen.tsimport type { CodegenConfig } from "@graphql-codegen/cli";const config: CodegenConfig = {schema: "https://api.example.com/graphql",documents: ["src/**/*.{ts,tsx}"],ignoreNoDocuments: true,generates: {"./src/gql/": {preset: "client",config: {documentMode: "string",enumsAsTypes: true,},presetConfig: {fragmentMasking: true,},},},};export default config;
documentMode ist entscheidend
documentMode: "string" mit Apollo Client oder urql zu verwenden führt dazu,
dass alle Query-Ergebnisse als any typisiert werden, weil diese Clients ein
TypedDocumentNode (AST-Objekt) erwarten, keinen einfachen String. Wenn deine
Typen als any auftauchen, prüfe diese Einstellung zuerst.
Du musst Codegen jedes Mal neu ausführen, wenn du eine Query änderst. Während der Entwicklung startest du es im Watch-Modus, damit die Typen automatisch synchron bleiben:
bashnpx graphql-codegen --watch
Verwendung mit Apollo Client
Apollo Client versteht TypedDocumentNode nativ, die Integration ist nahtlos (verwende die Standard-Konfiguration ohne documentMode: "string"):
tsximport { useQuery } from "@apollo/client";import { graphql } from "./gql";const AllFilmsQuery = graphql(`query AllFilms {allFilms {films {titlereleaseDate}}}`);function Films() {// data ist voll typisiert, keine Generics nötigconst { data } = useQuery(AllFilmsQuery);return (<ul>{data?.allFilms?.films?.map((film) => (<li key={film?.title}>{film?.title}</li>))}</ul>);}
Verwendung mit TanStack Query
TanStack Query hat keinen nativen GraphQL-Support, also schreibt man einmal eine kleine execute-Funktion. Dieser Ansatz verwendet documentMode: "string", das TypedDocumentString statt eines AST-Knotens generiert. Der String kann direkt über fetch gesendet werden:
tsimport type { TypedDocumentString } from "./gql/graphql";export async function execute<TResult, TVariables>(query: TypedDocumentString<TResult, TVariables>,variables?: TVariables,): Promise<TResult> {const response = await fetch("/graphql", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify({ query: query.toString(), variables }),});const { data } = await response.json();return data;}
Dann in Komponenten verwenden:
tsximport { useQuery } from "@tanstack/react-query";import { graphql } from "./gql";import { execute } from "./graphql-client";const PeopleQuery = graphql(`query AllPeople {allPeople {totalCountpeople {name}}}`);function People() {const { data } = useQuery({queryKey: ["allPeople"],queryFn: () => execute(PeopleQuery),});return <span>{data?.allPeople?.totalCount} Personen</span>;}
Fragment Masking
Das Client Preset unterstützt Fragment Masking, ein Pattern, bei dem jede Komponente die Daten deklariert, die sie braucht, und das Typsystem erzwingt, dass nur diese Komponente auf diese Felder zugreifen kann. Wir behandeln das ausführlich in einem eigenen Abschnitt weiter unten, weil es ein Problem löst, das weit über Codegen-Konfiguration hinausgeht.
Apollo Client und das Client Preset
Apollos Dokumentation weist darauf hin, dass das Client Preset zusätzlichen
Runtime-Code generiert, der mit Apollos normalisiertem Cache inkompatibel sein
kann. Falls Cache-Probleme auftreten, ziehe stattdessen die Legacy-Plugins
typescript + typescript-operations in Betracht. Siehe die
Apollo-Docs
für das empfohlene Setup.
gql.tada: Ganz ohne Codegen
gql.tada geht einen radikal anderen Weg: kein Codegen-Schritt. Stattdessen nutzt es TypeScripts Typsystem, um Ergebnis- und Variablentypen zur Compile-Time aus deinen GraphQL-Queries zu inferieren.
Wie es funktioniert
- Dein Schema wird über ein TypeScript-Plugin geladen
- Das Plugin generiert eine
graphql-env.d.ts-Typdatei - Wenn du
graphql('query { ... }')schreibst, parst TypeScript den Query-String auf Typebene - Ergebnis- und Variablentypen werden inferiert. Kein Build-Schritt, kein Watcher
Setup
bashnpm install gql.tada
tsconfig.json{"compilerOptions": {"plugins": [{"name": "gql.tada/ts-plugin","schema": "./schema.graphql","tadaOutputLocation": "./src/graphql-env.d.ts"}]}}
Verwendung
tsximport { graphql } from "gql.tada";const PokemonQuery = graphql(`query Pokemon($name: String!) {pokemon(name: $name) {idnametypes}}`);// Der Ergebnistyp wird vollständig inferiert:// { pokemon: { id: string; name: string; types: string[] } | null }
Du verwendest das typisierte Dokument mit jedem Client (urql, Apollo oder einem einfachen fetch-Wrapper mit TanStack Query). Der Hauptunterschied zu graphql-codegen: Es gibt keinen Build-Schritt. Deine Typen sind immer aktuell, weil sie von TypeScript selbst berechnet werden.
Wann gql.tada vs. graphql-codegen?
| gql.tada | graphql-codegen client-preset | |
|---|---|---|
| Codegen-Schritt | Keiner | Erforderlich (CLI oder Watcher) |
| Typen immer aktuell | Ja (von TS berechnet) | Erst nach Codegen-Lauf |
| Fragment-Handling | Explizit (als Argument übergeben) | Global (automatisch erkannt) |
| Persisted Documents | Via CLI | Eingebaut |
| Ökosystem-Reife | Neuer | Sehr ausgereift (~5M Downloads/Woche) |
| Editor-DX | Auto-Complete via TS-Plugin | Erfordert Codegen-Lauf für Typen |
| Große Schemas | Kann TS-Server verlangsamen | Kein TS-Performance-Impact |
Performance-Hinweis
Bei sehr großen Schemas kann gql.tadas Type-Level-Inferenz den TypeScript-Language-Server verlangsamen. Wenn dein Schema hunderte Typen und tief verschachtelte Queries hat, bietet graphql-codegen möglicherweise eine flüssigere Editor-Erfahrung, da die Typen vorberechnet sind.
Typisierte Daten an Kind-Komponenten weitergeben
Die Beispiele oben zeigen den Happy Path: Du rufst useQuery(AllFilmsQuery) auf und data ist perfekt inferiert, innerhalb dieser Komponente. Keine expliziten Typen nötig. Aber was passiert, wenn du diese Daten an Kind-Komponenten weitergeben musst?
Das ist die Frage, auf die jede typisierte GraphQL-Codebase irgendwann stößt, und die Antwort ist wichtiger als die Wahl des Codegen-Tools.
Der alte Weg: generierte Typen für alles
Mit den alten graphql-codegen-Plugins hat jede Query einen Satz TypeScript-Typen produziert: einen für das Gesamtergebnis, plus verschachtelte Typen für jede Ebene der Response:
ts// Generierte Typen (Legacy-Ansatz)type AllFilmsQuery = { allFilms: { films: AllFilmsQuery_allFilms_films[] } };type AllFilmsQuery_allFilms_films = { title: string; releaseDate: string };
Man hat diese generierten Typen importiert und als Prop-Typen in Kind-Komponenten verwendet. Das funktionierte, aber es schuf ein praktisches Problem: Jede leicht unterschiedliche Query generierte ihren eigenen Satz Typen. Eine FilmCard, die title und director brauchte, bekam einen anderen Typ als eine, die title und releaseDate brauchte. Am Ende hatte man Dutzende nahezu identische Typen, alle eng an bestimmte Query-Shapes gekoppelt, und jede Query-Änderung löste eine Kaskade von Typ-Updates quer durch die Komponenten aus.
Manche Teams versuchten, jeden Data-Fetch in einen Custom Hook zu wrappen (useFilmCard, useFilmList), um die Typisierung zu kapseln. Aber das tauscht nur eine Typ-Explosion gegen eine Hook-Explosion: viele leicht unterschiedliche Hooks, die nur existieren, um einen Typ zu tragen, ohne echte Logik zum Kapseln.
Der moderne Weg: Fragment Masking
Fragment Masking dreht das Ownership-Modell um. Statt dass die Query Typen diktiert, die nach unten fließen, deklariert jede Komponente die Daten, die sie braucht. Der Typ kommt vom Fragment, nicht vom Query-Ergebnis.
Hier ist ein vollständiges Beispiel mit drei Ebenen: eine Blatt-Komponente, eine Listen-Komponente und eine Seiten-Komponente:
1. Die Blatt-Komponente deklariert ihren Datenbedarf:
components/FilmCard.tsximport { graphql, FragmentType, useFragment } from "../gql";export const FilmCardFragment = graphql(`fragment FilmCard on Film {titlereleaseDatedirector}`);function FilmCard(props: { film: FragmentType<typeof FilmCardFragment> }) {const film = useFragment(FilmCardFragment, props.film);return (<div><h3>{film.title}</h3><p>Regie: {film.director}</p><time>{film.releaseDate}</time></div>);}
Der Prop-Typ FragmentType<typeof FilmCardFragment> ist opak. Die Eltern-Komponente kann nicht versehentlich auf film.director zugreifen, bevor sie es weitergibt. Nur FilmCard selbst kann die Daten über useFragment entpacken.
2. Eine mittlere Komponente hat ihr eigenes Fragment und komponiert das des Kindes:
components/FilmList.tsximport { graphql, FragmentType, useFragment } from "../gql";import FilmCard from "./FilmCard";export const FilmListFragment = graphql(`fragment FilmList on FilmsConnection {totalCountfilms {id...FilmCard}}`);function FilmList(props: { data: FragmentType<typeof FilmListFragment> }) {const connection = useFragment(FilmListFragment, props.data);return (<section><h2>{connection.totalCount} Filme</h2><ul>{connection.films?.map((film) => (<li key={film?.id}><FilmCard film={film} /></li>))}</ul></section>);}
FilmList spreaded ...FilmCard innerhalb seines eigenen Fragments. Es kann auf id und totalCount zugreifen (die es selbst deklariert hat), aber nicht auf director oder releaseDate (die gehören zu FilmCard). TypeScript erzwingt das zur Compile-Time.
3. Die Top-Level-Komponente besitzt die Query:
pages/FilmsPage.tsximport { useQuery } from "@apollo/client";import { graphql } from "../gql";import FilmList from "../components/FilmList";const AllFilmsQuery = graphql(`query AllFilms {allFilms {...FilmList}}`);function FilmsPage() {const { data, loading } = useQuery(AllFilmsQuery);if (loading) return <p>Lädt…</p>;if (!data?.allFilms) return <p>Keine Filme gefunden</p>;return <FilmList data={data.allFilms} />;}
FilmsPage kann überhaupt keine Film-Felder lesen. Die Daten sind vollständig opak. Es reicht sie einfach nach unten durch.
Warum das wichtig ist
Die Ownership-Kette sieht so aus:
| Ebene | Fragment | Besitzt | Spreaded |
|---|---|---|---|
| Seite | AllFilmsQuery | — | ...FilmList |
| Liste | FilmListFragment | id, totalCount | ...FilmCard |
| Karte | FilmCardFragment | title, releaseDate, director | — |
Das löst die Probleme, die generierte Typen und Custom-Hook-Wrapper geschaffen haben:
- Keine manuellen Typen nötig. Jeder Prop-Typ ist
FragmentType<typeof XFragment>. Keine generiertenAllFilmsQuery_allFilms_films-Typen, keinePick<>-Akrobatik, keine Typ-Explosion. - Sicheres Refactoring. Wenn
FilmCardeinrating-Feld zu seinem Fragment hinzufügt, lässt man Codegen laufen und die Query enthält es automatisch. Keine Änderungen in Eltern-Komponenten nötig. - Compile-Time-Grenzerzwingung. Jede Komponente sieht nur, was sie angefragt hat.
FilmsPagekann buchstäblich nicht auffilm.titlezugreifen. TypeScript lässt es nicht zu. - Keine Hook-Wrapper nötig. Das Fragment ist der Typvertrag. Typisierte Daten an ein Kind weitzugeben hat keine Logik zum Kapseln, also wäre ein Custom Hook dafür premature abstraction.
Das ist das Pattern, das Relay vor Jahren etabliert hat. Das graphql-codegen Client Preset bringt es ins breitere Ökosystem, ohne die Relay-Runtime zu benötigen. gql.tada unterstützt dieselbe Idee über readFragment().
Wann man keine Fragments braucht
Fragment Masking glänzt in Komponenten-Bäumen mit mehreren Ebenen. Für eine
einfache Seite, die Daten fetcht und direkt rendert, ohne Kind-Komponenten,
ist der inferierte Typ von useQuery völlig ausreichend. Füge nicht überall
Fragments hinzu, nur weil du es kannst.
Meine Empfehlung für 2026
Für REST / OpenAPI
Nutze @hey-api/openapi-ts mit dem TanStack-Query-Plugin. Der Options-basierte Ansatz ist sauber, kompositionsfähig und framework-agnostisch. Die DX ist hervorragend: Du bekommst volle Typsicherheit von deiner OpenAPI-Spec bis zur data-Property deiner Komponente.
openapi-ts.config.tsimport { defineConfig } from "@hey-api/openapi-ts";export default defineConfig({input: "./openapi.yaml",output: "src/client",plugins: ["@hey-api/typescript", "@hey-api/sdk", "@tanstack/react-query"],});
Füge es zu deinen package.json-Scripts hinzu:
json{"scripts": {"codegen:api": "openapi-ts"}}
Für GraphQL
Wenn dein Schema klein bis mittelgroß ist, probiere gql.tada. Die Zero-Codegen-Erfahrung ist unschlagbar für Entwicklungsgeschwindigkeit.
Wenn dein Schema groß ist oder du Persisted Documents und Fragment Masking in einem ausgereiften Setup brauchst, nutze graphql-codegen mit dem Client Preset.
In beiden Fällen: Nutze Fragment Masking für Komponenten-Komposition. Wenn dein Komponenten-Baum mehr als eine Ebene hat, sind Fragments der Weg, typisierte Daten weiterzugeben, ohne für jede Query-Shape Typen zu generieren oder alles in Custom Hooks zu wrappen. Das gilt für graphql-codegen (via FragmentType + useFragment) genauso wie für gql.tada (via readFragment).
Vermeide die alten Hook-generierenden Plugins. Sie werden bestenfalls von der Community gepflegt und sind schlimmstenfalls veraltet.
Validierungsschicht
Sowohl Hey API als auch graphql-codegen unterstützen die Generierung von Zod-Schemas aus deinen API-Definitionen. Das gibt dir Runtime-Validierung zusätzlich zu Compile-Time-Typen, nützlich an Systemgrenzen, wo man den Daten nicht vertrauen kann.
ts// openapi-ts.config.ts (Zod-Plugin hinzufügen)plugins: ['@hey-api/typescript','@hey-api/sdk','@tanstack/react-query','zod', // Runtime-Validierungsschemas],
Das Pattern auf einen Blick
Hier ist das mentale Modell:
REST:
OpenAPI-Spec →
@hey-api/openapi-ts→ Options-Objekte →useQuery({ ...options })
GraphQL (Codegen + Apollo/urql):
GraphQL-Schema →
graphql-codegen→TypedDocumentNode→useQuery(document)
GraphQL (Codegen + TanStack Query):
GraphQL-Schema →
graphql-codegen→TypedDocumentString→useQuery({ queryFn: () => execute(document) })
GraphQL (ohne Codegen):
GraphQL-Schema →
gql.tada→ inferierte Typen →useQuery({ queryFn: () => execute(document) })
Alle drei Wege enden gleich: ein framework-agnostisches Primitiv, das sich in die vorhandenen Hooks deiner Data-Fetching-Library einsteckt. Der Generator kümmert sich um Typsicherheit und API-Mapping. Dein Framework kümmert sich um Rendering und State.
Für Komponenten-Komposition in GraphQL kommt ein weiterer Pfeil hinzu:
Komponenten-Fragment → wird in Eltern-Fragment gespreaded → wird in Query gespreaded
Jede Komponente besitzt ihre Datenanforderungen über ein Fragment. Typen fließen von den Blättern nach oben, nicht von der Query nach unten. Keine generierten Per-Query-Typen, keine Custom-Hook-Wrapper. Nur Fragments.
Das ist die Separation of Concerns, auf die sich das Ökosystem 2026 geeinigt hat.
DX-Stolperfallen: Codegen im Editor zum Laufen bringen
Die Codegen-Story ist mittlerweile solide, aber das umgebende Tooling hat noch scharfe Kanten. Wer graphql-codegen im Watch-Modus nutzt, wird wahrscheinlich auf zwei Probleme stoßen: ESLint zeigt falsche Typfehler an, und VS Code bietet kein Autocomplete innerhalb von graphql()-Aufrufen.
Veraltete ESLint-Typen
graphql-codegen --watch regeneriert Typen laufend. Das ESLint-TypeScript-Programm cached Typinformationen und wird nach jeder Regenerierung veraltet. Das führt zu falsch-positiven Fehlern in jeder Datei, die useQuery oder useMutation verwendet, obwohl die Typen eigentlich korrekt sind.
Der Fix besteht aus zwei Teilen. Erstens: Auf projectService umstellen, damit ESLint mit dem TypeScript Language Service von VS Code synchron bleibt, statt sein eigenes veraltetes Programm zu pflegen:
jsparserOptions: {projectService: true,tsconfigRootDir: import.meta.dirname,}
Zweitens: Die no-unsafe-*-Regeln deaktivieren. Diese existieren, um eingeschlichene any-Typen zu finden, aber Codegen liefert saubere Typen, daher bringen sie keinen Mehrwert und erzeugen nur Rauschen:
jsrules: {"@typescript-eslint/no-unsafe-member-access": "off","@typescript-eslint/no-unsafe-assignment": "off","@typescript-eslint/no-unsafe-return": "off","@typescript-eslint/no-unsafe-argument": "off",}
Alle anderen typbewussten Regeln wie await-thenable funktionieren weiterhin normal.
VS Code Autocomplete für GraphQL
Die beliebte Extension graphql.vscode-graphql bietet kein Autocomplete innerhalb von graphql()-Template-Literal-Aufrufen. Stattdessen die Apollo GraphQL Extension (apollographql.vscode-apollo) verwenden. Konfiguration über eine apollo.config.js im Projekt-Root:
ui/apollo.config.jsmodule.exports = {client: {tagName: "graphql", // passt zur codegen-generierten graphql()-Funktionservice: {name: "my-app",localSchemaFile: "./api/schema.graphql",},includes: ["./src/**/*.{ts,tsx,js,jsx}"],},};
Das gibt dir schema-bewusstes Autocomplete, Validierung und Go-to-Definition in jedem graphql()-Aufruf.
Changelog
Dieser Artikel wird jährlich aktualisiert, um die neuesten Tools und Patterns widerzuspiegeln.
| Jahr | Wichtige Änderungen |
|---|---|
| 2026 | Erstausgabe. @hey-api/openapi-ts Options-Pattern, graphql-codegen Client Preset, gql.tada, Wechsel von Hooks zu Options. |
| 2026-03 | DX-Stolperfallen-Abschnitt hinzugefügt: ESLint veraltete Typen bei Codegen Watch-Modus, VS Code Autocomplete via Apollo Extension. |
Quellen & Weiterführendes
- @hey-api/openapi-ts Dokumentation
Offizielle Docs für den führenden OpenAPI-zu-TypeScript-Codegenerator.
- Hey API — TanStack Query Plugin
Wie man queryOptions und mutationOptions aus der OpenAPI-Spec generiert.
- GraphQL Code Generator — Client Preset
Der empfohlene Codegen-Ansatz für GraphQL in 2026.
- GraphQL Code Generator — v3/v5-Roadmap
Das GitHub-Issue, in dem The Guild erklärt hat, warum sie von generierten Hooks abgerückt sind.
- gql.tada Dokumentation
Typsicheres GraphQL ohne Codegen-Schritt, mittels TypeScript-Inferenz.
- TanStack Query v5 — Query Options
Der queryOptions()-Helper, der das Options-basierte Codegen-Pattern ermöglicht hat.
- Orval Dokumentation
OpenAPI-Codegenerator mit eingebauter MSW-Mock-Generierung.
- Orval — queryOptions Feature Request
Community-Diskussion über das Hinzufügen von Options-basiertem Output zu Orval.
