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

12 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).

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 — der 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

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;

Verwendung mit Apollo Client

Apollo Client versteht TypedDocumentNode nativ, die Integration ist nahtlos:

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:

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

Eines der besten Features des Client Presets ist Fragment Masking. Es erzwingt, dass Komponenten nur auf die Daten zugreifen können, die sie explizit per Fragment deklariert haben:

tsx
import { graphql, FragmentType, useFragment } from "./gql";
const FilmCardFragment = graphql(`
fragment FilmCard on Film {
title
releaseDate
director
}
`);
function FilmCard(props: { film: FragmentType<typeof FilmCardFragment> }) {
const film = useFragment(FilmCardFragment, props.film);
// film.title — typisiert und zugänglich
// film.id — Compile-Error, nicht im Fragment
return <h3>{film.title}</h3>;
}

Das ist ein Gamechanger für große Codebases. Es verhindert, dass Komponenten von Daten abhängen, die sie nicht angefragt haben, was das Refactoring von Queries sicher macht.

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

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()).

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: 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',
'@hey-api/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 (mit Codegen):

GraphQL-Schema → graphql-codegen → TypedDocumentNode → useQuery(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.

Das ist die Separation of Concerns, auf die sich das Ökosystem 2026 geeinigt hat.

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.

Quellen & Weiterführendes


S
Geschrieben von
Sascha Becker
Weitere Artikel