Typsichere GraphQL-APIs mit Code-Generierung erstellen
Praktischer Leitfaden für durchgängige Typsicherheit in GraphQL mit Schema-first-Codegenerierung: typisierte Resolver, Query-Typen, keine Typkonflikte.

Die Typ-Lücke in GraphQL-Anwendungen
GraphQL verspricht einen typisierten API-Vertrag, doch die meisten Implementierungen haben eine Lücke: Das Schema definiert Typen in SDL, die Resolver verwenden untypisierte JavaScript-Objekte, und der Client typisiert die Antwortformen manuell. Jede Abweichung zwischen diesen drei Schichten erzeugt Laufzeitfehler, die das Typsystem hätte abfangen müssen.
Code-Generierung schließt diese Lücke, indem sie TypeScript-Typen automatisch aus dem GraphQL-Schema ableitet – sowohl für Server-Resolver als auch für Client-Queries.
Schema-First-Design
# schema.graphql — the single source of truth
type User {
id: ID!
email: String!
name: String!
role: UserRole!
posts(limit: Int = 10, offset: Int = 0): [Post!]!
createdAt: DateTime!
}
enum UserRole {
ADMIN
EDITOR
VIEWER
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [String!]!
publishedAt: DateTime
status: PostStatus!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Query {
user(id: ID!): User
users(role: UserRole, limit: Int = 20): [User!]!
post(id: ID!): Post
posts(status: PostStatus, limit: Int = 20): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updatePost(id: ID!, input: UpdatePostInput!): Post!
publishPost(id: ID!): Post!
}
input CreateUserInput {
email: String!
name: String!
role: UserRole!
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}Resolver-Typen generieren
Die Code-Generierung verwandelt das Schema in TypeScript-Interfaces, die Resolver erfüllen müssen. Der Compiler erzwingt, dass jeder Resolver die korrekte Form zurückgibt – kein Hoffen mehr, dass der Resolver zum Schema passt.
// ❌ Untyped resolvers — schema/resolver mismatch is invisible
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await db.users.findById(id);
return user; // Does this match the User type? Who knows.
},
},
};
// ✅ Generated types enforce schema compliance
// Generated by graphql-codegen from schema.graphql
import type { Resolvers } from "./generated/resolvers-types";
const resolvers: Resolvers = {
Query: {
user: async (_parent, { id }, context) => {
const user = await context.db.users.findById(id);
if (!user) return null;
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role, // Must be UserRole enum
createdAt: user.createdAt,
// TypeScript error if any required field is missing
};
},
users: async (_parent, { role, limit }, context) => {
return context.db.users.findMany({
where: role ? { role } : undefined,
take: limit ?? 20,
});
},
},
User: {
posts: async (parent, { limit, offset }, context) => {
return context.db.posts.findMany({
where: { authorId: parent.id },
take: limit ?? 10,
skip: offset ?? 0,
});
},
},
Mutation: {
createUser: async (_parent, { input }, context) => {
return context.db.users.create({ data: input });
},
publishPost: async (_parent, { id }, context) => {
return context.db.posts.update({
where: { id },
data: { status: "PUBLISHED", publishedAt: new Date() },
});
},
},
};Codegen-Konfiguration
# codegen.yml
schema: "./schema.graphql"
generates:
# Server-side resolver types
./src/generated/resolvers-types.ts:
plugins:
- typescript
- typescript-resolvers
config:
contextType: "../context#GraphQLContext"
mappers:
User: "../models#UserModel"
Post: "../models#PostModel"
scalars:
DateTime: "Date"
# Client-side operation types
./src/generated/operations.ts:
documents: "./src/**/*.graphql"
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
scalars:
DateTime: "string"Typsicherheit auf der Client-Seite
Dasselbe Schema generiert typisierte Hooks für den Client. Query-Ergebnisse, Mutation-Inputs und Variablen werden alle gegen das Schema geprüft – Frontend und Backend sind garantiert in ihren Formen abgestimmt.
// src/queries/user.graphql
// query GetUser($id: ID!) {
// user(id: $id) {
// id
// email
// name
// role
// posts(limit: 5) {
// id
// title
// status
// }
// }
// }
// Generated hook — fully typed
import { useGetUserQuery } from "../generated/operations";
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId }, // Type-checked
});
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (!data?.user) return <NotFound />;
const { user } = data;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<span>{user.role}</span> {/* Typed as UserRole enum */}
<ul>
{user.posts.map((post) => (
<li key={post.id}>
{post.title} — {post.status}
</li>
))}
</ul>
</div>
);
}Umgang mit Schema-Evolution
Typen ändern sich, wenn sich das Produkt weiterentwickelt. Die Code-Generierung verwandelt Schema-Änderungen in Compiler-Fehler und macht jede Breaking Change sofort sichtbar.
interface SchemaChange {
type: "field-added" | "field-removed" | "type-changed" | "field-deprecated";
path: string;
breaking: boolean;
}
function analyzeSchemaChanges(
oldSchema: string,
newSchema: string
): SchemaChange[] {
const changes: SchemaChange[] = [];
// Adding a nullable field — non-breaking
// Adding a required field — breaking for mutations
// Removing a field — breaking for queries using it
// Changing a field type — breaking
// After codegen runs, TypeScript surfaces the impact:
// - Removed field? Every resolver and query referencing it errors
// - Type changed? Every consumer with wrong type errors
// - New required input? Every mutation call missing it errors
return changes;
}
// CI pipeline: regenerate types on schema changes
// 1. Developer modifies schema.graphql
// 2. CI runs graphql-codegen
// 3. TypeScript compilation checks all resolvers and client queries
// 4. Build fails if any consumer doesn't match the new schema
// 5. Developer fixes consumers before mergingWichtige Erkenntnisse
Die GraphQL-Code-Generierung beseitigt die Typ-Lücke zwischen Schema, Resolvern und Client-Queries. Das Schema wird zur einzigen Quelle der Wahrheit, und jeder TypeScript-Typ wird automatisch daraus abgeleitet. Rückgabetypen der Resolver werden erzwungen – fehlende Felder oder falsche Typen sind Compiler-Fehler, keine Laufzeitbugs.
Führe die Code-Generierung in CI aus, damit jede Schema-Änderung frische Typen erzeugt. Wenn ein Feld entfernt oder sein Typ geändert wird, identifiziert der TypeScript-Compiler sofort jeden betroffenen Resolver und jede Client-Query. Das macht Schema-Evolution von einem manuellen Audit zu einer automatisierten Prüfung.
Die Investition ist eine Codegen-Konfigurationsdatei und ein Build-Schritt. Der Ertrag ist durchgängige Typsicherheit über jede Schicht des GraphQL-Stacks hinweg – eine ganze Klasse von Bugs, die sonst erst in der Produktion auftauchen würde, wird eliminiert.


