API & GRAPHQL

GraphQL mit Claude Code: TypeScript API-Entwicklung 2026

đź“… 6. Mai 2026 ⏱ 12 min Lesezeit đź›  TypeScript · GraphQL · Apollo

đź“‹ Inhaltsverzeichnis

  1. GraphQL Schema Design mit SDL
  2. Resolver-Implementierung mit TypeScript
  3. Pothos Schema Builder — Code-First
  4. GraphQL Code Generator
  5. Apollo Client Integration
  6. Subscriptions & Real-Time

GraphQL hat sich als Standard für moderne APIs durchgesetzt — und Claude Code beschleunigt die Entwicklung dramatisch. Dieser Artikel zeigt, wie du 2026 mit Claude Code produktionsreife TypeScript GraphQL-APIs baust: von Schema-First Design über Resolver und DataLoader bis hin zu Subscriptions und Code Generator. Alle Codebeispiele sind direkt produktionsbereit.

đź”·
Schema-First Design SDL, Typen, Interfaces
⚙️
Resolver & DataLoader N+1 Problem lösen
đź§±
Pothos Builder Type-safe Code-First
🔄
Real-Time Subscriptions WebSocket, PubSub

1. GraphQL Schema Design mit SDL

Der erste Schritt bei jeder GraphQL-API ist das Schema. Claude Code hilft dir, ein sauberes, konsistentes Schema zu entwerfen — mit Scalars, Enums, Input Types und klaren Interfaces. Hier zeigen wir ein vollständiges Beispiel für eine Produktmanagement-API.

SDL SCHEMA schema.graphql
# Scalars & Direktiven scalar DateTime scalar JSON scalar Upload directive @auth(requires: Role = USER) on FIELD_DEFINITION directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE # Enums enum Role { ADMIN MANAGER USER } enum ProductStatus { DRAFT PUBLISHED ARCHIVED } enum SortOrder { ASC DESC } # Interface interface Node { id: ID! } interface Timestamps { createdAt: DateTime! updatedAt: DateTime! } # Types type User implements Node & Timestamps { id: ID! email: String! name: String! role: Role! products: [Product!]! createdAt: DateTime! updatedAt: DateTime! } type Product implements Node & Timestamps { id: ID! title: String! description: String price: Float! status: ProductStatus! tags: [String!]! metadata: JSON owner: User! variants: [ProductVariant!]! createdAt: DateTime! updatedAt: DateTime! } type ProductVariant implements Node { id: ID! sku: String! price: Float! stock: Int! attributes: JSON! } # Pagination type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type ProductConnection { edges: [ProductEdge!]! pageInfo: PageInfo! totalCount: Int! } type ProductEdge { node: Product! cursor: String! } # Input Types input CreateProductInput { title: String! description: String price: Float! status: ProductStatus = DRAFT tags: [String!] = [] metadata: JSON } input UpdateProductInput { title: String description: String price: Float status: ProductStatus tags: [String!] metadata: JSON } input ProductFilterInput { status: ProductStatus minPrice: Float maxPrice: Float tags: [String!] ownerId: ID search: String } input PaginationInput { first: Int after: String last: Int before: String } input ProductSortInput { field: ProductSortField! order: SortOrder = DESC } enum ProductSortField { CREATED_AT UPDATED_AT PRICE TITLE } # Root Types type Query { me: User @auth user(id: ID!): User @auth(requires: ADMIN) users: [User!]! @auth(requires: ADMIN) product(id: ID!): Product products( filter: ProductFilterInput sort: ProductSortInput pagination: PaginationInput ): ProductConnection! searchProducts(query: String!, limit: Int = 10): [Product!]! } type Mutation { createProduct(input: CreateProductInput!): Product! @auth updateProduct(id: ID!, input: UpdateProductInput!): Product! @auth deleteProduct(id: ID!): Boolean! @auth publishProduct(id: ID!): Product! @auth(requires: MANAGER) uploadProductImage(productId: ID!, file: Upload!): String! @auth } type Subscription { productUpdated(id: ID!): Product! productStatusChanged(status: ProductStatus): Product! newProductCreated: Product! }
đź’ˇ Claude Code Tipp: Lass Claude Code dein Schema reviewen und auf N+1-Probleme, fehlende Null-Safety und Pagination-Antipatterns prĂĽfen. Einfach das Schema einfĂĽgen und fragen: "Welche Performance-Probleme siehst du?"

Schema-Validierung mit TypeScript

Nach dem SDL-Design empfiehlt sich eine automatische Validierung beim Start des Servers:

// src/schema/validate.ts import { buildSchema, validateSchema } from 'graphql'; import { readFileSync } from 'fs'; import { join } from 'path'; export function loadAndValidateSchema() { const sdl = readFileSync(join(__dirname, 'schema.graphql'), 'utf8'); const schema = buildSchema(sdl); const errors = validateSchema(schema); if (errors.length > 0) { console.error('❌ Schema-Validierungsfehler:'); errors.forEach(e => console.error(e.message)); process.exit(1); } console.log('✅ Schema valide — starte Server'); return schema; }

2. Resolver-Implementierung mit TypeScript

Resolver sind das Herzstück jeder GraphQL-API. Mit TypeScript und Claude Code bekommst du vollständig typisierte Resolver mit DataLoader für N+1-Prevention, Context-Typing und sauberer Fehlerbehandlung — alles in einem konsistenten Pattern.

RESOLVER src/resolvers/types.ts
// Context Typing — SSOT für alle Resolver import { PrismaClient } from '@prisma/client'; import DataLoader from 'dataloader'; import { User } from './generated/graphql'; export interface DataLoaders { userLoader: DataLoader<string, User>; productsByOwnerLoader: DataLoader<string, Product[]>; variantsByProductLoader: DataLoader<string, ProductVariant[]>; } export interface GraphQLContext { prisma: PrismaClient; currentUser: User | null; loaders: DataLoaders; requestId: string; } // ResolverMap — vollständig typisiert import { Resolvers } from './generated/graphql'; export type TypedResolvers = Resolvers<GraphQLContext>;
RESOLVER src/resolvers/product.resolver.ts
// Vollständige Product-Resolver mit DataLoader import { TypedResolvers, GraphQLContext } from './types'; import { AuthenticationError, UserInputError, ForbiddenError } from 'apollo-server-core'; import { encodeCursor, decodeCursor } from '../utils/cursor'; import { pubsub, EVENTS } from '../pubsub'; export const productResolvers: TypedResolvers = { Query: { product: async (_parent, { id }, ctx: GraphQLContext) => { return ctx.prisma.product.findUnique({ where: { id } }); }, products: async (_parent, { filter, sort, pagination }, ctx: GraphQLContext) => { const { first = 20, after } = pagination ?? {}; const cursor = after ? decodeCursor(after) : undefined; const where = buildProductWhere(filter); const orderBy = sort ? { [sort.field.toLowerCase()]: sort.order.toLowerCase() } : { createdAt: 'desc' as const }; const [items, totalCount] = await ctx.prisma.$transaction([ ctx.prisma.product.findMany({ where, orderBy, take: first + 1, skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, }), ctx.prisma.product.count({ where }), ]); const hasNextPage = items.length > first; const edges = items.slice(0, first).map(node => ({ node, cursor: encodeCursor(node.id), })); return { edges, totalCount, pageInfo: { hasNextPage, hasPreviousPage: !!cursor, startCursor: edges[0]?.cursor ?? null, endCursor: edges[edges.length - 1]?.cursor ?? null, }, }; }, searchProducts: async (_parent, { query, limit }, ctx: GraphQLContext) => { return ctx.prisma.product.findMany({ where: { OR: [ { title: { contains: query, mode: 'insensitive' } }, { description: { contains: query, mode: 'insensitive' } }, { tags: { hasSome: [query] } }, ], status: 'PUBLISHED', }, take: limit, orderBy: { createdAt: 'desc' }, }); }, }, Mutation: { createProduct: async (_parent, { input }, ctx: GraphQLContext) => { if (!ctx.currentUser) throw new AuthenticationError('Nicht authentifiziert'); const product = await ctx.prisma.product.create({ data: { ...input, ownerId: ctx.currentUser.id, }, }); await pubsub.publish(EVENTS.PRODUCT_CREATED, { newProductCreated: product }); return product; }, updateProduct: async (_parent, { id, input }, ctx: GraphQLContext) => { if (!ctx.currentUser) throw new AuthenticationError('Nicht authentifiziert'); const existing = await ctx.prisma.product.findUnique({ where: { id } }); if (!existing) throw new UserInputError(`Produkt ${id} nicht gefunden`); if (existing.ownerId !== ctx.currentUser.id && ctx.currentUser.role !== 'ADMIN') { throw new ForbiddenError('Keine Berechtigung'); } const updated = await ctx.prisma.product.update({ where: { id }, data: { ...input, updatedAt: new Date() }, }); await pubsub.publish(EVENTS.PRODUCT_UPDATED, { productUpdated: updated }); return updated; }, deleteProduct: async (_parent, { id }, ctx: GraphQLContext) => { if (!ctx.currentUser) throw new AuthenticationError('Nicht authentifiziert'); await ctx.prisma.product.delete({ where: { id } }); return true; }, }, // Field Resolver — DataLoader für N+1 Prevention Product: { owner: (parent, _args, ctx: GraphQLContext) => { return ctx.loaders.userLoader.load(parent.ownerId); }, variants: (parent, _args, ctx: GraphQLContext) => { return ctx.loaders.variantsByProductLoader.load(parent.id); }, }, }; // DataLoader Factory — verhindert N+1 Queries export function createDataLoaders(prisma: PrismaClient): DataLoaders { return { userLoader: new DataLoader<string, User>(async (ids) => { const users = await prisma.user.findMany({ where: { id: { in: [...ids] } }, }); const byId = Object.fromEntries(users.map(u => [u.id, u])); return ids.map(id => byId[id] ?? new Error(`User ${id} not found`)); }), productsByOwnerLoader: new DataLoader(async (ownerIds) => { const products = await prisma.product.findMany({ where: { ownerId: { in: [...ownerIds] } }, }); const byOwner = ownerIds.map(id => products.filter(p => p.ownerId === id)); return byOwner; }), variantsByProductLoader: new DataLoader(async (productIds) => { const variants = await prisma.productVariant.findMany({ where: { productId: { in: [...productIds] } }, }); const byProduct = productIds.map(id => variants.filter(v => v.productId === id)); return byProduct; }), }; }
⚠️ N+1-Problem: Ohne DataLoader führt jede product.owner-Anfrage eine separate DB-Query durch. Bei 100 Produkten = 101 Queries statt 2. Claude Code erkennt dieses Muster und schlägt automatisch DataLoader vor.

3. Pothos Schema Builder — Code-First mit TypeScript

Pothos ist 2026 der beste Code-First-Ansatz für GraphQL mit TypeScript. Vollständige Typsicherheit ohne Code-Generator, native Prisma-Integration und Plugin-Architektur für Auth, Complexity-Limits und mehr. Claude Code versteht das Pothos-API perfekt.

POTHOS src/schema/builder.ts
// Pothos Builder Setup mit allen Plugins import SchemaBuilder from '@pothos/core'; import PrismaPlugin from '@pothos/plugin-prisma'; import ScopeAuthPlugin from '@pothos/plugin-scope-auth'; import RelayPlugin from '@pothos/plugin-relay'; import ComplexityPlugin from '@pothos/plugin-complexity'; import ValidationPlugin from '@pothos/plugin-validation'; import { DateTimeResolver, JSONResolver } from 'graphql-scalars'; import { PrismaClient } from '@prisma/client'; import type { GraphQLContext } from './context'; import prismaTypes from './prisma-types'; // generated export const builder = new SchemaBuilder<{ Context: GraphQLContext; PrismaTypes: typeof prismaTypes; AuthScopes: { isAuthenticated: boolean; hasRole: 'ADMIN' | 'MANAGER' | 'USER'; }; Scalars: { DateTime: { Input: Date; Output: Date }; JSON: { Input: unknown; Output: unknown }; }; }>({ plugins: [ScopeAuthPlugin, PrismaPlugin, RelayPlugin, ComplexityPlugin, ValidationPlugin], authScopes: (ctx: GraphQLContext) => ({ isAuthenticated: !!ctx.currentUser, hasRole: (role) => ctx.currentUser?.role === role, }), prisma: { client: (ctx) => ctx.prisma }, complexity: { defaultComplexity: 1, defaultListMultiplier: 10, limit: (ctx) => ({ complexity: ctx.currentUser?.role === 'ADMIN' ? 10000 : 2000, depth: 8, breadth: 40, }), }, relayOptions: { clientMutationId: 'omit', cursorType: 'String', }, validationOptions: { useZod: true }, }); // Scalars registrieren builder.addScalarType('DateTime', DateTimeResolver, {}); builder.addScalarType('JSON', JSONResolver, {});
POTHOS src/schema/product.type.ts
// Pothos Prisma-Object mit Relay Connection import { builder } from './builder'; const ProductStatusEnum = builder.enumType('ProductStatus', { values: ['DRAFT', 'PUBLISHED', 'ARCHIVED'] as const, }); const ProductRef = builder.prismaNode('Product', { id: { field: 'id' }, authScopes: { isAuthenticated: false }, // public read fields: (t) => ({ title: t.exposeString('title'), description: t.exposeString('description', { nullable: true }), price: t.exposeFloat('price'), status: t.expose('status', { type: ProductStatusEnum }), tags: t.exposeStringList('tags'), createdAt: t.expose('createdAt', { type: 'DateTime' }), updatedAt: t.expose('updatedAt', { type: 'DateTime' }), // Relation — Prisma Plugin löst N+1 automatisch owner: t.relation('owner'), variants: t.relation('variants'), // Berechnetes Feld priceFormatted: t.string({ resolve: (product) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }) .format(product.price), }), }), }); // Query Fields builder.queryField('product', (t) => t.prismaField({ type: 'Product', nullable: true, args: { id: t.arg.id({ required: true }), }, resolve: (query, _root, { id }, ctx) => ctx.prisma.product.findUnique({ ...query, where: { id: String(id) } }), }) ); builder.queryField('products', (t) => t.prismaConnection({ type: 'Product', cursor: 'id', args: { status: t.arg({ type: ProductStatusEnum, required: false }), minPrice: t.arg.float({ required: false }), search: t.arg.string({ required: false }), }, resolve: (query, _root, { status, minPrice, search }, ctx) => ctx.prisma.product.findMany({ ...query, where: { ...(status ? { status } : {}), ...(minPrice ? { price: { gte: minPrice } } : {}), ...(search ? { OR: [ { title: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, ] } : {}), }, orderBy: { createdAt: 'desc' }, }), }) ); // Mutation Fields const CreateProductInput = builder.inputType('CreateProductInput', { fields: (t) => ({ title: t.string({ required: true, validate: { minLength: 3, maxLength: 200 } }), description: t.string({ required: false }), price: t.float({ required: true, validate: { min: 0 } }), status: t.field({ type: ProductStatusEnum, defaultValue: 'DRAFT' }), tags: t.stringList({ required: false, defaultValue: [] }), }), }); builder.mutationField('createProduct', (t) => t.prismaField({ type: 'Product', authScopes: { isAuthenticated: true }, args: { input: t.arg({ type: CreateProductInput, required: true }), }, resolve: async (query, _root, { input }, ctx) => { return ctx.prisma.product.create({ ...query, data: { ...input, ownerId: ctx.currentUser!.id, }, }); }, }) );
💡 Pothos vs. SDL-First: Pothos eignet sich besonders für komplexe APIs mit vielen Relationen. SDL-First ist besser wenn dein Team schema.graphql als Kommunikationsmittel nutzt (z.B. Mobile + Backend + Frontend). Claude Code unterstützt beide Ansätze vollständig.

4. GraphQL Code Generator

GraphQL Code Generator erzeugt aus deinem Schema und deinen Operations vollständig typisierte TypeScript-Typen, Hooks und Document Nodes. Claude Code hilft dir, die optimale codegen.yml-Konfiguration zu erstellen und zu warten.

CODEGEN codegen.yml
overwrite: true schema: "./src/schema/schema.graphql" documents: "./src/**/*.{graphql,tsx,ts}" generates: # Backend: vollständige Resolver-Typen ./src/generated/graphql.ts: plugins: - typescript - typescript-resolvers config: contextType: "../context#GraphQLContext" mappers: User: "@prisma/client#User" Product: "@prisma/client#Product" ProductVariant: "@prisma/client#ProductVariant" enumsAsConst: true useIndexSignature: true scalars: DateTime: Date JSON: unknown Upload: Promise<FileUpload> # Frontend: typed-document-node für Apollo/urql ./src/generated/operations.ts: plugins: - typescript - typescript-operations - typed-document-node config: documentMode: documentNodeImportFragments optimizeDocumentNode: true pureMagicComment: true dedupeFragments: true # React Hooks mit Apollo ./src/generated/apollo-hooks.tsx: plugins: - typescript - typescript-operations - typescript-react-apollo config: withHooks: true withRefetchFn: true hooksSuffix: Hook apolloReactHooksImportFrom: "@apollo/client" # Client-side schema (für Introspection / Apollo Studio) ./src/generated/schema.json: plugins: - introspection
CODEGEN src/operations/products.graphql
# Fragments — Wiederverwendbare Feldmengen fragment ProductCore on Product { id title description price priceFormatted status tags createdAt } fragment ProductFull on Product { ...ProductCore owner { id name email } variants { id sku price stock } } # Query mit Variablen query GetProducts( $status: ProductStatus $first: Int = 20 $after: String $search: String ) { products( filter: { status: $status, search: $search } pagination: { first: $first, after: $after } ) { edges { node { ...ProductFull } cursor } pageInfo { hasNextPage endCursor } totalCount } } query GetProduct($id: ID!) { product(id: $id) { ...ProductFull } } # Mutations mutation CreateProduct($input: CreateProductInput!) { createProduct(input: $input) { ...ProductFull } } mutation UpdateProduct($id: ID!, $input: UpdateProductInput!) { updateProduct(id: $id, input: $input) { ...ProductFull } } mutation DeleteProduct($id: ID!) { deleteProduct(id: $id) } # Subscription subscription OnProductUpdated($id: ID!) { productUpdated(id: $id) { ...ProductCore updatedAt } }
đź’ˇ Claude Code + Codegen: Claude Code kann automatisch Operations aus deinen React-Komponenten extrahieren und typisierte Hooks generieren. Prompt: "Analysiere diese Komponente und erstelle die passenden GraphQL Operations + codegen.yml-Eintrag."

5. Apollo Client Integration

Apollo Client ist 2026 weiterhin der Standard für GraphQL im Frontend. Mit TypeScript, InMemoryCache-Konfiguration, optimistic responses und reactive variables erhältst du eine vollständig reaktive UI mit minimalem Boilerplate.

APOLLO CLIENT src/apollo/client.ts
// Apollo Client Setup — vollständig typisiert import { ApolloClient, InMemoryCache, HttpLink, split, makeVar, Reference, } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; import { createClient } from 'graphql-ws'; import { onError } from '@apollo/client/link/error'; import { RetryLink } from '@apollo/client/link/retry'; import type { ProductStatus } from '../generated/graphql'; // Reactive Variables — globaler State ohne Redux export const filterStatusVar = makeVar<ProductStatus | null>(null); export const searchQueryVar = makeVar<string>(''); export const isAuthenticatedVar = makeVar<boolean>(false); // HTTP Link mit Auth const httpLink = new HttpLink({ uri: process.env.NEXT_PUBLIC_GRAPHQL_URL ?? '/api/graphql', credentials: 'include', headers: { 'x-client-version': '1.0.0', }, }); // WebSocket Link für Subscriptions const wsLink = new GraphQLWsLink( createClient({ url: process.env.NEXT_PUBLIC_GRAPHQL_WS_URL ?? 'ws://localhost:4000/graphql', connectionParams: () => ({ authorization: localStorage.getItem('auth-token'), }), retryAttempts: 5, shouldRetry: () => true, }) ); // Error Link const errorLink = onError(({ graphQLErrors, networkError, operation }) => { if (graphQLErrors) { graphQLErrors.forEach(({ message, extensions }) => { if (extensions?.code === 'UNAUTHENTICATED') { isAuthenticatedVar(false); window.location.href = '/login'; } console.error(`[GraphQL Error] ${operation.operationName}: ${message}`); }); } if (networkError) console.error(`[Network Error]: ${networkError}`); }); // Retry Link für transiente Fehler const retryLink = new RetryLink({ delay: { initial: 300, max: 3000, jitter: true }, attempts: { max: 3, retryIf: (error) => !!error }, }); // Split: HTTP für Queries/Mutations, WS für Subscriptions const splitLink = split( ({ query }) => { const def = getMainDefinition(query); return def.kind === 'OperationDefinition' && def.operation === 'subscription'; }, wsLink, errorLink.concat(retryLink.concat(httpLink)) ); // Cache-Konfiguration mit Type Policies export const cache = new InMemoryCache({ typePolicies: { Query: { fields: { products: { // Relay Cursor Pagination Merge keyArgs: ['filter', 'sort'], merge(existing, incoming, { args }) { if (!args?.pagination?.after) return incoming; return { ...incoming, edges: [...(existing?.edges ?? []), ...incoming.edges], }; }, read(existing) { return existing; }, }, filterStatus: { read: () => filterStatusVar() }, searchQuery: { read: () => searchQueryVar() }, }, }, Product: { keyFields: ['id'], }, User: { keyFields: ['id'], }, }, }); export const apolloClient = new ApolloClient({ link: splitLink, cache, defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network', errorPolicy: 'all', notifyOnNetworkStatusChange: true, }, query: { fetchPolicy: 'network-only', errorPolicy: 'all', }, mutate: { errorPolicy: 'all', }, }, connectToDevTools: process.env.NODE_ENV === 'development', });
APOLLO CLIENT src/components/ProductList.tsx
// React-Komponente mit generierten Hooks + Optimistic UI import { useReactiveVar } from '@apollo/client'; import { useGetProductsQuery, useCreateProductMutation, useDeleteProductMutation, GetProductsDocument, } from '../generated/apollo-hooks'; import { filterStatusVar, searchQueryVar } from '../apollo/client'; export function ProductList() { const status = useReactiveVar(filterStatusVar); const search = useReactiveVar(searchQueryVar); const { data, loading, error, fetchMore } = useGetProductsQuery({ variables: { status, search, first: 20 }, notifyOnNetworkStatusChange: true, }); const [createProduct] = useCreateProductMutation({ // Optimistic Response — UI sofort aktualisieren optimisticResponse: (vars) => ({ createProduct: { __typename: 'Product' as const, id: `temp-${Date.now()}`, title: vars.input.title, description: vars.input.description ?? null, price: vars.input.price, priceFormatted: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(vars.input.price), status: 'DRAFT' as const, tags: vars.input.tags ?? [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), owner: { __typename: 'User', id: 'current', name: 'Du', email: '' }, variants: [], }, }), update: (cache, { data }) => { if (!data?.createProduct) return; cache.modify({ fields: { products(existing) { const newEdge = { __typename: 'ProductEdge', node: data.createProduct, cursor: data.createProduct.id, }; return { ...existing, edges: [newEdge, ...existing.edges], totalCount: existing.totalCount + 1, }; }, }, }); }, }); const [deleteProduct] = useDeleteProductMutation({ update: (cache, _result, { variables }) => { cache.evict({ id: cache.identify({ __typename: 'Product', id: variables?.id }) }); cache.gc(); }, }); const loadMore = () => { if (!data?.products.pageInfo.hasNextPage) return; fetchMore({ variables: { after: data.products.pageInfo.endCursor, }, }); }; if (loading && !data) return <div>Lade Produkte...</div>; if (error) return <div>Fehler: {error.message}</div>; return ( <div> <div className="filters"> <input value={search} onChange={(e) => searchQueryVar(e.target.value)} placeholder="Suche..." /> </div> <div className="product-grid"> {data?.products.edges.map(({ node }) => ( <div key={node.id}> <h3>{node.title}</h3> <p>{node.priceFormatted}</p> <button onClick={() => deleteProduct({ variables: { id: node.id } })}> Löschen </button> </div> ))} </div> {data?.products.pageInfo.hasNextPage && ( <button onClick={loadMore}>Mehr laden</button> )} </div> ); }

6. Subscriptions & Real-Time

GraphQL Subscriptions ermöglichen Echtzeit-Updates via WebSocket. Mit graphql-ws, PubSub und asyncIterator baust du skalierbare Real-Time-Features — von Live-Updates bis hin zu Kollaborations-Features. Claude Code generiert die gesamte Subscription-Infrastruktur in Minuten.

SUBSCRIPTION SERVER src/pubsub.ts
// PubSub Setup — skalierbar mit Redis für Prod import { PubSub } from 'graphql-subscriptions'; import { RedisPubSub } from 'graphql-redis-subscriptions'; import Redis from 'ioredis'; // Event Registry — alle Subscription-Events typisiert export const EVENTS = { PRODUCT_UPDATED: 'PRODUCT_UPDATED', PRODUCT_STATUS_CHANGED: 'PRODUCT_STATUS_CHANGED', PRODUCT_CREATED: 'PRODUCT_CREATED', } as const; export type EventName = typeof EVENTS[keyof typeof EVENTS]; // Production: Redis PubSub (horizontal skalierbar) function createPubSub() { if (process.env.REDIS_URL) { const publisher = new Redis(process.env.REDIS_URL); const subscriber = new Redis(process.env.REDIS_URL); return new RedisPubSub({ publisher, subscriber }); } // Development: In-Memory PubSub return new PubSub(); } export const pubsub = createPubSub();
SUBSCRIPTION RESOLVERS src/resolvers/subscription.resolver.ts
// Subscription Resolver mit withFilter + asyncIterator import { withFilter } from 'graphql-subscriptions'; import { pubsub, EVENTS } from '../pubsub'; import type { TypedResolvers, GraphQLContext } from './types'; export const subscriptionResolvers: TypedResolvers['Subscription'] = { productUpdated: { // Subscribe mit Filter — nur relevante Updates subscribe: withFilter( () => pubsub.asyncIterator([EVENTS.PRODUCT_UPDATED]), (payload, variables) => { return payload.productUpdated.id === variables.id; } ), resolve: (payload) => payload.productUpdated, }, productStatusChanged: { subscribe: withFilter( () => pubsub.asyncIterator([EVENTS.PRODUCT_STATUS_CHANGED]), (payload, variables) => { if (!variables.status) return true; // alle Status wenn kein Filter return payload.productStatusChanged.status === variables.status; } ), resolve: (payload) => payload.productStatusChanged, }, newProductCreated: { subscribe: (_parent, _args, ctx: GraphQLContext) => { // Authentifizierung für Subscriptions if (!ctx.currentUser) { throw new Error('Nicht authentifiziert'); } return pubsub.asyncIterator([EVENTS.PRODUCT_CREATED]); }, resolve: (payload) => payload.newProductCreated, }, };
WEBSOCKET SERVER src/server.ts
// Vollständiger Server mit HTTP + WebSocket import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; import express from 'express'; import http from 'http'; import cors from 'cors'; import { typeDefs, resolvers } from './schema'; import { createContext } from './context'; import { createDataLoaders } from './resolvers/product.resolver'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const app = express(); const httpServer = http.createServer(app); const schema = makeExecutableSchema({ typeDefs, resolvers }); // WebSocket Server für Subscriptions const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql', }); const serverCleanup = useServer( { schema, context: async (ctx) => { // Auth aus WebSocket Connection Params const token = ctx.connectionParams?.authorization as string; const currentUser = token ? await verifyToken(token) : null; return { prisma, currentUser, loaders: createDataLoaders(prisma), requestId: crypto.randomUUID(), }; }, onConnect: (ctx) => { console.log(`WS connected: ${ctx.connectionParams?.clientId ?? 'anon'}`); return true; }, onDisconnect: (_ctx, code, reason) => { console.log(`WS disconnected: ${code} ${reason}`); }, }, wsServer ); // Apollo Server const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); }, }; }, }, ], }); await server.start(); app.use( '/graphql', cors({ origin: process.env.CORS_ORIGIN, credentials: true }), express.json(), expressMiddleware(server, { context: async ({ req }) => ({ prisma, currentUser: await getUserFromRequest(req), loaders: createDataLoaders(prisma), requestId: req.headers['x-request-id'] as string ?? crypto.randomUUID(), }), }) ); httpServer.listen(4000, () => { console.log('🚀 GraphQL: http://localhost:4000/graphql'); console.log('🔌 WebSocket: ws://localhost:4000/graphql'); });
REACT SUBSCRIPTION src/components/ProductDetail.tsx
// React Komponente mit Live-Updates via Subscription import { useGetProductQuery, useOnProductUpdatedSubscription } from '../generated/apollo-hooks'; interface Props { productId: string } export function ProductDetail({ productId }: Props) { const { data, loading } = useGetProductQuery({ variables: { id: productId }, }); // Subscription — automatisches Cache-Update useOnProductUpdatedSubscription({ variables: { id: productId }, onData: ({ client, data: subData }) => { if (!subData.data?.productUpdated) return; // Cache direkt schreiben — kein Re-fetch nötig client.cache.writeFragment({ id: client.cache.identify({ __typename: 'Product', id: productId, }), fragment: ProductCoreFragmentDoc, data: subData.data.productUpdated, }); }, }); if (loading) return <div>Lade...</div>; const product = data?.product; if (!product) return <div>Produkt nicht gefunden</div>; return ( <div> <h1>{product.title}</h1> <span className={`status-badge ${product.status.toLowerCase()}`> {product.status} </span> <p>{product.priceFormatted}</p> <p>Zuletzt aktualisiert: {new Date(product.updatedAt).toLocaleString('de-DE')}</p> </div> ); }
⚠️ Production-Tipp: In-Memory PubSub funktioniert nur auf einer Server-Instanz. Für horizontale Skalierung (mehrere Pods) IMMER Redis PubSub verwenden. Claude Code erkennt diesen Fehler und schlägt die richtige Konfiguration vor wenn du nach "Skalierung" fragst.
💡 Subscription Security: Auth-Token NIEMALS in der WebSocket-URL mitgeben (wird geloggt!). Immer über connectionParams senden — genau wie im obigen Beispiel. Claude Code weist dich automatisch auf dieses Sicherheitsproblem hin.
GraphQL TypeScript Apollo Pothos DataLoader Subscriptions WebSocket Code Generator Prisma Claude Code
AM
Agentic Movers Redaktion Claude Code Mastery — praxisnahe Tutorials seit 2025

GraphQL-Modul im Kurs

Im Claude Code Mastery Kurs: vollständiges GraphQL-Modul mit Pothos Schema Builder, Code Generator, Apollo Client, Subscriptions und DataLoader-Optimierung.

14 Tage kostenlos testen →