2026

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.

S
Sascha Becker
Author

18 Min. Lesezeit

Typsichere API-Codegenerierung für React in 2026

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:

ts
import { queryOptions } from "@tanstack/react-query";
const todosOptions = queryOptions({
queryKey: ["todos"],
queryFn: fetchTodos,
staleTime: 5000,
});
// In einem Hook verwenden
const { data } = useQuery(todosOptions);
// Zum Prefetching verwenden
await 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.ts
import { 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:

bash
npx @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.ts
export 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:

tsx
import { 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 } }),
);

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.ts
import { 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-tsOrval
OutputOptions-ObjekteEigene Hooks
TanStack QueryReact, Vue, Svelte, Solid, AngularReact, Vue, Svelte, Solid
Mock-GenerierungSeparates PluginEingebaut (MSW + Faker.js)
ValidierungZod, ValibotZod
HTTP-ClientsFetch, Axios, Ky, Next.js, NuxtAxios, Fetch
ReifePre-1.0, schnelle Entwicklungv8, 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:

  1. Das Client Preset nutzen, um eine typisierte graphql()-Funktion zu generieren
  2. Queries inline mit dieser Funktion schreiben
  3. 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) generiert TypedDocumentNode-Objekte (AST). Apollo Client und urql verstehen diese nativ.
  • documentMode: "string" generiert TypedDocumentString-Werte (einfache Strings mit Typ-Metadaten). Das ist leichtgewichtiger, funktioniert aber nur mit Clients, die Strings akzeptieren, wie ein eigener fetch-Wrapper für TanStack Query.

Für Apollo Client oder urql (generiert TypedDocumentNode):

codegen.ts
import 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.ts
import 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;

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:

bash
npx graphql-codegen --watch

Verwendung mit Apollo Client

Apollo Client versteht TypedDocumentNode nativ, die Integration ist nahtlos (verwende die Standard-Konfiguration ohne documentMode: "string"):

tsx
import { useQuery } from "@apollo/client";
import { graphql } from "./gql";
const AllFilmsQuery = graphql(`
query AllFilms {
allFilms {
films {
title
releaseDate
}
}
}
`);
function Films() {
// data ist voll typisiert, keine Generics nötig
const { 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:

ts
import 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:

tsx
import { useQuery } from "@tanstack/react-query";
import { graphql } from "./gql";
import { execute } from "./graphql-client";
const PeopleQuery = graphql(`
query AllPeople {
allPeople {
totalCount
people {
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.

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

  1. Dein Schema wird über ein TypeScript-Plugin geladen
  2. Das Plugin generiert eine graphql-env.d.ts-Typdatei
  3. Wenn du graphql('query { ... }') schreibst, parst TypeScript den Query-String auf Typebene
  4. Ergebnis- und Variablentypen werden inferiert. Kein Build-Schritt, kein Watcher

Setup

bash
npm install gql.tada
tsconfig.json
{
"compilerOptions": {
"plugins": [
{
"name": "gql.tada/ts-plugin",
"schema": "./schema.graphql",
"tadaOutputLocation": "./src/graphql-env.d.ts"
}
]
}
}

Verwendung

tsx
import { graphql } from "gql.tada";
const PokemonQuery = graphql(`
query Pokemon($name: String!) {
pokemon(name: $name) {
id
name
types
}
}
`);
// 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.tadagraphql-codegen client-preset
Codegen-SchrittKeinerErforderlich (CLI oder Watcher)
Typen immer aktuellJa (von TS berechnet)Erst nach Codegen-Lauf
Fragment-HandlingExplizit (als Argument übergeben)Global (automatisch erkannt)
Persisted DocumentsVia CLIEingebaut
Ökosystem-ReifeNeuerSehr ausgereift (~5M Downloads/Woche)
Editor-DXAuto-Complete via TS-PluginErfordert Codegen-Lauf für Typen
Große SchemasKann TS-Server verlangsamenKein TS-Performance-Impact

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.tsx
import { graphql, FragmentType, useFragment } from "../gql";
export const FilmCardFragment = graphql(`
fragment FilmCard on Film {
title
releaseDate
director
}
`);
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.tsx
import { graphql, FragmentType, useFragment } from "../gql";
import FilmCard from "./FilmCard";
export const FilmListFragment = graphql(`
fragment FilmList on FilmsConnection {
totalCount
films {
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.tsx
import { 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:

EbeneFragmentBesitztSpreaded
SeiteAllFilmsQuery...FilmList
ListeFilmListFragmentid, totalCount...FilmCard
KarteFilmCardFragmenttitle, 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 generierten AllFilmsQuery_allFilms_films-Typen, keine Pick<>-Akrobatik, keine Typ-Explosion.
  • Sicheres Refactoring. Wenn FilmCard ein rating-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. FilmsPage kann buchstäblich nicht auf film.title zugreifen. 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().

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.ts
import { 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-codegenTypedDocumentNodeuseQuery(document)

GraphQL (Codegen + TanStack Query):

GraphQL-Schema → graphql-codegenTypedDocumentStringuseQuery({ 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:

js
parserOptions: {
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:

js
rules: {
"@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.js
module.exports = {
client: {
tagName: "graphql", // passt zur codegen-generierten graphql()-Funktion
service: {
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.

JahrWichtige Änderungen
2026Erstausgabe. @hey-api/openapi-ts Options-Pattern, graphql-codegen Client Preset, gql.tada, Wechsel von Hooks zu Options.
2026-03DX-Stolperfallen-Abschnitt hinzugefügt: ESLint veraltete Typen bei Codegen Watch-Modus, VS Code Autocomplete via Apollo Extension.

Quellen & Weiterführendes


S
Geschrieben von
Sascha Becker
Weitere Artikel