GraphQL mit Claude Code: Apollo Client & Schema-Design 2026

Von Schema-Design über Apollo Client bis DataLoader — wie Claude Code dich bei jedem Schritt einer modernen GraphQL-API begleitet und Boilerplate auf ein Minimum reduziert.

GraphQL hat REST in vielen modernen Projekten längst abgelöst — aber das volle Potenzial entfaltet es erst, wenn Schema-Design, Resolver-Logik und Client-seitiges Caching sauber zusammenspielen. Genau hier ist Claude Code besonders stark: Es versteht die Semantik von GraphQL-SDL, generiert typsichere Resolver, erklärt das N+1-Problem und schreibt DataLoader-Code, der in Produktion sofort funktioniert.

Dieser Artikel zeigt, wie du mit Claude Code eine vollständige GraphQL-API mit Apollo Server und Apollo Client aufbaust — von der ersten Type-Definition bis zu Subscriptions und Performance-Optimierungen.

1. GraphQL Schema-Design mit SDL-First

Der erste Schritt jeder GraphQL-API ist das Schema. Claude Code hilft dir, ein konsistentes SDL (Schema Definition Language) zu entwerfen, das alle Anforderungen abbildet — inklusive Mutations, Subscriptions und wiederverwendbaren Input Types.

SDL Basis-Schema: Typen, Queries und Mutations

Claude Code-Prompt: "Erstelle ein GraphQL-Schema für eine Blog-Plattform mit Users, Posts und Comments. Inkl. Pagination und Input-Types."

# schema.graphql — generiert von Claude Code scalar DateTime type User { id: ID! email: String! name: String! posts: [Post!]! createdAt: DateTime! } type Post { id: ID! title: String! content: String! author: User! comments: [Comment!]! tags: [String!]! published: Boolean! createdAt: DateTime! } type Comment { id: ID! body: String! author: User! post: Post! createdAt: DateTime! } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { node: Post! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } input CreatePostInput { title: String! content: String! tags: [String!] published: Boolean = false } input UpdatePostInput { title: String content: String tags: [String!] published: Boolean } input PostsFilterInput { authorId: ID tags: [String!] published: Boolean search: String } type Query { post(id: ID!): Post posts( first: Int = 10 after: String filter: PostsFilterInput ): PostConnection! user(id: ID!): User me: User } type Mutation { createPost(input: CreatePostInput!): Post! updatePost(id: ID!, input: UpdatePostInput!): Post! deletePost(id: ID!): Boolean! addComment(postId: ID!, body: String!): Comment! } type Subscription { commentAdded(postId: ID!): Comment! postPublished: Post! }
Claude Code-Tipp: Beschreibe dein Datenmodell in natürlicher Sprache — Claude Code schlägt automatisch sinnvolle Relay-kompatible Pagination (Cursor-based), wiederverwendbare Input-Types und Non-Null-Markierungen vor. Du kannst direkt im Chat iterieren: "Füge ein Rating-Feld zu Post hinzu und ein AggregateType für Statistiken."

2. Apollo Server Setup mit Resolvern und Context

Mit dem Schema als Grundlage baut Claude Code den Apollo Server inklusive typsicherer Resolver, Auth-Context und strukturierter Fehlerbehandlung. Das spart Stunden an Boilerplate.

Server Apollo Server 4 mit TypeScript

// src/server.ts import { ApolloServer } from '@apollo/server' import { startStandaloneServer } from '@apollo/server/standalone' import { readFileSync } from 'fs' import { resolvers } from './resolvers' import { createContext } from './context' import { createLoaders } from './loaders' const typeDefs = readFileSync('./schema.graphql', 'utf-8') const server = new ApolloServer({ typeDefs, resolvers, formatError: (formattedError, error) => { // Sensible Fehler in Produktion maskieren if (process.env.NODE_ENV === 'production') { return { message: 'Internal server error', code: formattedError.extensions?.code } } return formattedError }, }) const { url } = await startStandaloneServer(server, { context: async ({ req }) => { const ctx = await createContext(req) return { ...ctx, loaders: createLoaders(ctx.db), // DataLoader per Request } }, listen: { port: 4000 }, }) console.log(`Server ready at: ${url}`)

Server Resolver-Struktur mit Auth-Guard

// src/resolvers/post.ts import { GraphQLError } from 'graphql' import { Resolvers } from '../generated/graphql' export const postResolvers: Resolvers = { Query: { post: async (_, { id }, { db, loaders }) => { return loaders.post.load(id) }, posts: async (_, { first, after, filter }, { db }) => { const posts = await db.post.findMany({ take: first + 1, cursor: after ? { id: after } : undefined, where: { published: filter?.published ?? true, authorId: filter?.authorId, tags: filter?.tags ? { hasSome: filter.tags } : undefined, OR: filter?.search ? [ { title: { contains: filter.search, mode: 'insensitive' } }, { content: { contains: filter.search, mode: 'insensitive' } }, ] : undefined, }, orderBy: { createdAt: 'desc' }, }) const hasNextPage = posts.length > first const edges = posts.slice(0, first).map(post => ({ node: post, cursor: post.id, })) return { edges, totalCount: await db.post.count(), pageInfo: { hasNextPage, hasPreviousPage: !!after, startCursor: edges[0]?.cursor, endCursor: edges[edges.length - 1]?.cursor, }, } }, }, Mutation: { createPost: async (_, { input }, { db, user }) => { if (!user) throw new GraphQLError('Nicht angemeldet', { extensions: { code: 'UNAUTHENTICATED' }, }) return db.post.create({ data: { ...input, authorId: user.id, tags: input.tags ?? [] }, }) }, }, Post: { // Field Resolver nutzt DataLoader statt N+1-Query author: (post, _, { loaders }) => loaders.user.load(post.authorId), comments: (post, _, { loaders }) => loaders.commentsByPost.load(post.id), }, }

3. Das N+1-Problem mit DataLoader lösen

Das N+1-Problem ist das häufigste Performance-Anti-Pattern in GraphQL: Für eine Liste von 100 Posts werden 100 separate DB-Queries für die Autoren ausgelöst. Claude Code erklärt nicht nur das Problem — es generiert sofort den DataLoader-Code.

Perf DataLoader — Batching & Caching

// src/loaders/index.ts import DataLoader from 'dataloader' import { PrismaClient } from '@prisma/client' export function createLoaders(db: PrismaClient) { return { // Statt N einzelner User-Queries → 1 Batch-Query für alle IDs user: new DataLoader<string, User>( async (ids) => { const users = await db.user.findMany({ where: { id: { in: [...ids] } }, }) const userMap = Object.fromEntries(users.map(u => [u.id, u])) return ids.map(id => userMap[id] ?? new Error(`User ${id} not found`)) }, { cache: true } // Request-scoped Cache ), post: new DataLoader<string, Post>( async (ids) => { const posts = await db.post.findMany({ where: { id: { in: [...ids] } }, }) const postMap = Object.fromEntries(posts.map(p => [p.id, p])) return ids.map(id => postMap[id] ?? new Error(`Post ${id} not found`)) } ), // 1:N Loader — alle Comments für mehrere Posts in einem Query commentsByPost: new DataLoader<string, Comment[]>( async (postIds) => { const comments = await db.comment.findMany({ where: { postId: { in: [...postIds] } }, orderBy: { createdAt: 'asc' }, }) return postIds.map(id => comments.filter(c => c.postId === id) ) } ), } } // Ergebnis: 100 Posts → 1 User-Query + 1 Comment-Query statt 200 Queries!
Wichtig: DataLoader muss per Request neu erstellt werden (nicht als Singleton). Sonst teilt sich der Cache über Request-Grenzen hinweg — und User A sieht User Bs Daten. Claude Code erstellt den Loader immer im Context-Factory-Pattern, wie im Server-Setup zu sehen.

4. Apollo Client: useQuery, useMutation und Cache-Management

Auf der Client-Seite generiert Claude Code vollständige React-Komponenten mit Apollo Hooks, typsicheren Queries aus generierten Types und sauberem Cache-Update-Code nach Mutations.

Client Apollo Client Setup und InMemoryCache

// src/apollo/client.ts import { ApolloClient, InMemoryCache, createHttpLink, split } from '@apollo/client' import { GraphQLWsLink } from '@apollo/client/link/subscriptions' import { getMainDefinition } from '@apollo/client/utilities' import { createClient } from 'graphql-ws' import { setContext } from '@apollo/client/link/context' const httpLink = createHttpLink({ uri: '/graphql' }) const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/graphql', connectionParams: () => ({ authorization: localStorage.getItem('token'), }), })) const authLink = setContext((_, { headers }) => ({ headers: { ...headers, authorization: localStorage.getItem('token') ?? '', }, })) // HTTP für Queries/Mutations, WebSocket für Subscriptions const splitLink = split( ({ query }) => { const def = getMainDefinition(query) return def.kind === 'OperationDefinition' && def.operation === 'subscription' }, wsLink, authLink.concat(httpLink), ) export const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache({ typePolicies: { Query: { fields: { posts: { // Cursor-based Pagination merge policy keyArgs: ['filter'], merge(existing, incoming) { return { ...incoming, edges: [...(existing?.edges ?? []), ...incoming.edges], } }, }, }, }, }, }), })

Client useMutation mit Optimistic Updates & Cache-Update

// src/components/PostList.tsx import { useQuery, useMutation, gql } from '@apollo/client' const GET_POSTS = gql` query GetPosts($first: Int, $filter: PostsFilterInput) { posts(first: $first, filter: $filter) { edges { node { id title author { name } tags createdAt } } pageInfo { hasNextPage endCursor } totalCount } } ` const ADD_COMMENT = gql` mutation AddComment($postId: ID!, $body: String!) { addComment(postId: $postId, body: $body) { id body author { name } createdAt } } ` export function PostList() { const { data, loading, fetchMore } = useQuery(GET_POSTS, { variables: { first: 10, filter: { published: true } }, }) const [addComment] = useMutation(ADD_COMMENT, { // Optimistic UI: Comment erscheint sofort vor Server-Antwort optimisticResponse: ({ postId, body }) => ({ addComment: { __typename: 'Comment', id: `temp-${Date.now()}`, body, author: { __typename: 'User', name: 'Du' }, createdAt: new Date().toISOString(), }, }), update(cache, { data: { addComment } }, { variables: { postId } }) { cache.modify({ id: cache.identify({ __typename: 'Post', id: postId }), fields: { comments: (existing = []) => [...existing, cache.writeFragment({ data: addComment, fragment: gql`fragment NewComment on Comment { id body author { name } createdAt }`, })], }, }) }, }) if (loading) return <span>Laden...</span> return ( <div> {data?.posts.edges.map(({ node }) => ( <article key={node.id}> <h2>{node.title}</h2> <p>Von {node.author.name}</p> </article> ))} {data?.posts.pageInfo.hasNextPage && ( <button onClick={() => fetchMore({ variables: { after: data.posts.pageInfo.endCursor } })}>Mehr laden</button> )} </div> ) }

5. Code-First mit TypeGraphQL

Beim Code-First-Ansatz entsteht das Schema automatisch aus TypeScript-Klassen. Claude Code ist ideal dafür: Es versteht Decorator-Patterns und generiert die vollständige Klassen-Hierarchie inklusive Validation durch class-validator.

SDL TypeGraphQL — Decorator-based Schema-Generierung

// src/schema/Post.ts — Schema direkt aus TypeScript import { ObjectType, Field, ID, InputType, Resolver, Query, Mutation, Arg, Ctx } from 'type-graphql' import { IsNotEmpty, MinLength, ArrayMaxSize } from 'class-validator' @ObjectType() export class Post { @Field(() => ID) id: string @Field() title: string @Field() content: string @Field(() => [String]) tags: string[] @Field() published: boolean @Field() createdAt: Date } @InputType() export class CreatePostInput { @Field() @IsNotEmpty() @MinLength(5, { message: 'Titel zu kurz (min. 5 Zeichen)' }) title: string @Field() @MinLength(20) content: string @Field(() => [String], { nullable: true }) @ArrayMaxSize(10) tags?: string[] } @Resolver(Post) export class PostResolver { @Query(() => Post, { nullable: true }) async post(@Arg('id', () => ID) id: string, @Ctx() { db }: Context) { return db.post.findUnique({ where: { id } }) } @Mutation(() => Post) async createPost(@Arg('input') input: CreatePostInput, @Ctx() { db, user }: Context) { if (!user) throw new Error('Nicht angemeldet') return db.post.create({ data: { ...input, authorId: user.id } }) } }
Code-First vs. Schema-First: Schema-First (SDL) eignet sich gut wenn mehrere Teams (Frontend/Backend) das Schema gemeinsam besitzen. Code-First mit TypeGraphQL ist ideal wenn TypeScript die einzige Sprache ist — du vermeidest Typ-Duplizierung komplett. Claude Code beherrscht beide Ansätze und kann auch zwischen ihnen konvertieren.

6. Subscriptions: Real-Time mit WebSocket und PubSub

GraphQL Subscriptions ermöglichen echte Real-Time-Updates — für Chats, Live-Dashboards oder kollaborative Features. Claude Code generiert den vollständigen PubSub-Setup inklusive Subscription-Resolver und Client-seitigem useSubscription-Hook.

Server PubSub und Subscription Resolver

// src/pubsub.ts import { PubSub } from 'graphql-subscriptions' export const pubsub = new PubSub() export const EVENTS = { COMMENT_ADDED: 'COMMENT_ADDED', POST_PUBLISHED: 'POST_PUBLISHED', } as const // src/resolvers/subscription.ts import { withFilter } from 'graphql-subscriptions' export const subscriptionResolvers = { Subscription: { commentAdded: { // withFilter: Nur Events für den angefragten Post durchlassen subscribe: withFilter( () => pubsub.asyncIterator([EVENTS.COMMENT_ADDED]), (payload, variables) => payload.commentAdded.postId === variables.postId ), }, postPublished: { subscribe: () => pubsub.asyncIterator([EVENTS.POST_PUBLISHED]), }, }, } // In der addComment-Mutation: PubSub-Event auslösen addComment: async (_, { postId, body }, { db, user }) => { const comment = await db.comment.create({ data: { postId, body, authorId: user.id }, include: { author: true }, }) // Alle Subscriber für diesen Post erhalten sofort das Update await pubsub.publish(EVENTS.COMMENT_ADDED, { commentAdded: comment }) return comment },

Client useSubscription im React-Frontend

// src/components/CommentSection.tsx import { useSubscription, gql } from '@apollo/client' const COMMENT_ADDED = gql` subscription OnCommentAdded($postId: ID!) { commentAdded(postId: $postId) { id body author { name } createdAt } } ` export function CommentSection({ postId }: { postId: string }) { useSubscription(COMMENT_ADDED, { variables: { postId }, onData: ({ client, data }) => { // Neuen Comment direkt in Apollo Cache schreiben client.cache.modify({ id: client.cache.identify({ __typename: 'Post', id: postId }), fields: { comments: (existing = []) => [ ...existing, { __ref: client.cache.identify(data.data!.commentAdded) } ], }, }) }, }) return <section><h3>Kommentare (Live)</h3></section> }

7. Performance: Persisted Queries, Query-Komplexität und Response-Caching

In Produktion braucht eine GraphQL-API drei Sicherheits- und Performance-Schichten. Claude Code hilft dir, alle drei in wenigen Minuten zu konfigurieren.

Perf Query Complexity Limits — DoS-Schutz

// src/server.ts — Query Complexity Plugin import { createComplexityPlugin, fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity' const complexityPlugin = createComplexityPlugin({ maximumComplexity: 1000, onComplete: (complexity) => { console.log(`Query complexity: ${complexity}`) }, createError: (max, actual) => new GraphQLError(`Query zu komplex: ${actual} (max ${max})`, { extensions: { code: 'QUERY_TOO_COMPLEX' }, }), estimators: [ fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 }), ], }) // Persisted Queries — Clients senden nur Hash, kein Query-String // Spart Bandbreite + verhindert Ad-hoc-Queries in Produktion import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries' import { generatePersistedQueryIdsFromManifest } from '@apollo/persisted-query-lists' const persistedQueryLink = createPersistedQueryLink({ generateHash: generatePersistedQueryIdsFromManifest({ loadManifest: () => import('./persisted-query-manifest.json'), }), })

Perf Response Caching mit Redis-Backend

// Schema-Direktive für Field-Level Caching type Query { # 60s gecacht für alle anonymen Requests posts(first: Int, filter: PostsFilterInput): PostConnection! @cacheControl(maxAge: 60) post(id: ID!): Post @cacheControl(maxAge: 30) # me = private, nie cachen! me: User @cacheControl(maxAge: 0, scope: PRIVATE) } // Apollo Server Response Cache Plugin mit Redis import responseCachePlugin from '@apollo/server-plugin-response-cache' import { KeyvAdapter } from '@apollo/utils.keyvadapter' import Keyv from 'keyv' const server = new ApolloServer({ typeDefs, resolvers, plugins: [ complexityPlugin, responseCachePlugin({ // Authenticated Requests erhalten eigenen Cache-Key sessionId: ({ contextValue }) => contextValue.user?.id ?? null, }), ], cache: new KeyvAdapter(new Keyv('redis://localhost:6379')), })
GraphQL-Performance-Checkliste:
✅ DataLoader für alle Relationen (N+1 eliminiert)
✅ Query Complexity Limit (DoS-Schutz, max. 1000 Punkte)
✅ Persisted Queries in Produktion (keine Ad-hoc-Queries)
✅ Response Cache mit Redis (field-level, scope-aware)
✅ Apollo Client InMemoryCache mit TypePolicies (kein Over-Fetching)

Fazit: Claude Code als GraphQL-Copilot

GraphQL-Expertise aufzubauen kostet normalerweise Monate. Mit Claude Code verkürzt sich der Weg dramatisch: Du beschreibst dein Datenmodell, Claude Code generiert SDL, typsichere Resolver, DataLoader-Konfigurationen und Apollo-Client-Code — in einem konsistenten, produktionsreifen Stil.

Besonders wertvoll ist Claude Codes Fähigkeit, das N+1-Problem automatisch zu erkennen und DataLoader-Code vorzuschlagen, bevor du das erste Performanceproblem erlebst. Und wenn du zwischen Code-First und Schema-First wechseln möchtest, kann Claude Code auch das — inklusive Migrationsstrategie.

Der nächste Schritt: Schema-Stitching und Federation für Microservices-Architekturen. Auch das beherrscht Claude Code — aber das ist ein eigener Artikel wert.

GraphQL-Modul im Kurs

Unser Kurs enthält ein vollständiges GraphQL-Modul: Schema-Design, Apollo Server & Client, DataLoader, Subscriptions und Performance-Optimierung — alles mit Claude Code als Copilot. Inklusive hands-on Projekte, die du direkt in dein Portfolio übernehmen kannst.

14 Tage kostenlos testen →