Skip to content
IRC-CodingIRC-Coding
GraphQL API EntwicklungGraphQL SchemasResolversSubscriptionsApollo GraphQL

GraphQL API Entwicklung Grundlagen: Schemas, Resolvers, Subscriptions & Apollo

GraphQL API Entwicklung Grundlagen mit Schemas, Resolvers, Subscriptions und Apollo. Query, Mutation, Subscription, Type System mit praktischen Beispielen.

S

schutzgeist

38 min read
GraphQL API Entwicklung Grundlagen: Schemas, Resolvers, Subscriptions & Apollo

GraphQL API Entwicklung Grundlagen: Schemas, Resolvers, Subscriptions & Apollo

Dieser Beitrag ist eine umfassende Einführung in die GraphQL API Entwicklung Grundlagen – inklusive Schemas, Resolvers, Subscriptions und Apollo mit praktischen Beispielen.

In a Nutshell

GraphQL ist eine Abfragesprache für APIs und eine serverseitige Laufzeitumgebung zur Ausführung dieser Abfragen mit einem typbasierten System zur Definition von Datenstrukturen.

Was ist GraphQL genau? Framework, Software oder Idee?

GraphQL ist kein Framework und keine fertige Software, die Du einfach installierst und sofort verwendest. Es ist eine Abfragesprache und eine Spezifikation, die genau beschreibt, wie Clients Daten von einem Server anfragen, wie der Server diese Anfragen verarbeitet und welche Antworten zurückkommen.

Stell es Dir so vor: GraphQL ist wie SQL für Datenbanken, aber eben für APIs. Es definiert die Syntax für Abfragen und das Regelwerk für die Ausführung. Du musst GraphQL selbst in einer Programmiersprache implementieren oder auf bestehende Bibliotheken und Frameworks zurückgreifen.

Apollo ist zum Beispiel eine populäre Plattform und ein Framework, das GraphQL implementiert. Es gibt Dir Apollo Server für den Backend-Bereich und Apollo Client für den Frontend-Bereich. Andere Implementierungen sind Relay, GraphQL Yoga oder urql. GraphQL selbst bleibt dabei unabhängig von der Programmiersprache oder dem Framework.

Kurz gesagt: GraphQL ist eine Idee und ein offener Standard für APIs, der es Clients ermöglicht, genau die Daten abzufragen, die sie wirklich brauchen.

Kompakte Fachbeschreibung

GraphQL ist ein query language für APIs, das Clients ermöglicht, genau die Daten anzufordern, die sie benötigen, und alles in einer einzigen Anfrage zu erhalten.

Kernkomponenten:

GraphQL Schema

  • Types: Datenstrukturen definieren
  • Queries: Datenabruf-Operationen
  • Mutations: Datenänderungs-Operationen
  • Subscriptions: Echtzeit-Updates
  • Interfaces: Wiederverwendbare Typen
  • Unions: Typ-Gruppierungen

Resolvers

  • Field Resolvers: Daten für einzelne Felder auflösen
  • Type Resolvers: Typ-Implementierungen
  • Context: Gemeinsamer Kontext für Resolvers
  • Data Loaders: N+1 Problem vermeiden
  • Middleware: Authentifizierung und Validierung

Apollo GraphQL

  • Apollo Server: GraphQL Server für Node.js
  • Apollo Client: GraphQL Client für Web/Mobile
  • Apollo Federation: Distributed GraphQL
  • Apollo Studio: GraphQL Monitoring
  • Apollo Gateway: GraphQL Gateway

Prüfungsrelevante Stichpunkte

  • GraphQL: Query language für APIs mit typbasiertem System
  • Schema: Typdefinitionen für Datenstrukturen
  • Query: Datenabruf-Operation mit spezifischen Feldern
  • Mutation: Datenänderungs-Operationen
  • Subscription: Echtzeit-Datenübertragung
  • Resolver: Funktionen zur Datenauflösung
  • Apollo: GraphQL Platform für Server und Client
  • Type System: Strenge Typisierung für Daten
  • IHK-relevant: Moderne API-Entwicklung und -Architektur

Kernkomponenten

  1. Schema Definition: GraphQL Type System und SDL Das Schema ist das Herzstück jeder GraphQL-API. Es beschreibt mit der Schema Definition Language (SDL), welche Datenstrukturen es gibt und welche Operationen erlaubt sind. Es definiert Object Types, Queries, Mutations, Subscriptions, Enums, Interfaces und Unions. Das Schema fungiert als Vertrag zwischen Client und Server.

  2. Query Execution: Query Parsing und Execution Wenn ein Client eine Query sendet, wird diese zuerst geparst und gegen das Schema validiert. Der Execution Engine bestimmt dann, welche Resolver in welcher Reihenfolge aufgerufen werden, um die angefragten Daten zurückzugeben. Dabei wird der GraphQL-Abfragebaum traversiert.

  3. Resolver Functions: Datenauflösung und Business Logic Jedes Feld in einem GraphQL-Schema kann einen Resolver besitzen. Resolver sind Funktionen, die die tatsächlichen Daten für ein Feld liefern. Sie können Daten aus Datenbanken, externen APIs oder anderen Quellen laden und enthalten oft die Business Logic der Anwendung.

  4. Subscriptions: WebSocket-basierte Echtzeit-Updates Subscriptions ermöglichen es dem Server, Echtzeit-Events an Clients zu pushen. Typischerweise basieren sie auf WebSockets. Sie werden verwendet, wenn Clients sofort über Änderungen informiert werden sollen, zum Beispiel bei neuen Nachrichten oder Status-Updates.

  5. Apollo Server: GraphQL Server Implementierung Apollo Server ist ein beliebtes Node.js-Framework, das eine GraphQL-API basierend auf einem Schema und Resolvers bereitstellt. Es bietet Features wie Authentifizierung, Fehlerbehandlung, Subscriptions, Caching und Plugin-Integration.

  6. Apollo Client: GraphQL Client mit Caching Apollo Client ist eine Bibliothek für JavaScript/TypeScript-Anwendungen, die es ermöglicht, GraphQL-Queries auszuführen, Daten zu cachen und UI-Komponenten automatisch zu aktualisieren, wenn sich Daten ändern. Der Cache reduziert unnötige Netzwerkrequests.

  7. Data Loading: Optimierung von Datenabfragen Bei komplexen GraphQL-Queries kann das sogenannte N+1-Problem entstehen, wenn für jedes Objekt eine separate Datenbankabfrage gemacht wird. Data Loaders bündeln diese Anfragen und minimieren so die Anzahl der Datenbankzugriffe.

  8. Error Handling: Fehlerbehandlung und Validation GraphQL führt teilweise auch bei fehlerhaften Feldern eine Antwort zurück. Errors werden in einem eigenen errors-Array zurückgegeben. Zusätzlich können eigene Validierungen, Authentifizierungsfehler und Autorisierungsfehler in den Resolvern behandelt werden.

Was ist Apollo und warum solltest Du es nutzen?

Apollo ist eine umfassende Plattform für GraphQL, die von der Firma Apollo GraphQL Inc. entwickelt wird. Sie besteht aus mehreren Teilen, die zusammen den gesamten GraphQL-Workflow abdecken: Apollo Server für das Backend, Apollo Client für das Frontend und Apollo Studio sowie Apollo Federation für Monitoring und verteilte Architekturen.

Wo bekommst Du Apollo?

  • Apollo Server und Apollo Client sind als Open-Source-Pakete über npm verfügbar.
  • Apollo Studio bietet eine cloudbasierte Oberfläche unter https://www.apollographql.com/.
  • Die Dokumentation und weitere Tools findest Du auf der offiziellen Apollo-Website.

Wer nutzt Apollo wofür?

  • Backend-Entwickler nutzen Apollo Server, um GraphQL-APIs schnell und typsicher aufzubauen.
  • Frontend-Entwickler nutzen Apollo Client, um Daten aus GraphQL-APIs in React, Vue, Angular oder anderen Frameworks zu laden und zu verwalten.
  • DevOps und Architekten nutzen Apollo Studio und Federation, um APIs zu überwachen, zu dokumentieren und über mehrere Microservices hinweg zu verteilen.

Warum solltest Du Apollo nutzen?

  • Apollo baut auf dem offiziellen GraphQL-Standard auf und ist einer der am weitesten verbreiteten Stacks.
  • Apollo Server vereinfacht das Erstellen von Schemas, Resolvern, Authentifizierung und Subscriptions.
  • Apollo Client reduziert durch intelligentes Caching und Zustandsmanagement den Aufwand im Frontend erheblich.
  • Apollo Studio hilft, Fehler zu finden, Performance zu messen und das Schema zu dokumentieren.
  • Apollo Federation ermöglicht es, große APIs aus kleinen, eigenverantwortlichen Services aufzubauen.

Kurz gesagt: Apollo ist das bekannteste Ökosystem rund um GraphQL und eignet sich besonders gut, wenn Du schnell eine produktionsreife, gut dokumentierte und skalierbare API bauen möchtest.

Praxisbeispiele

In den folgenden Beispielen siehst Du, wie eine GraphQL-API in der Praxis aufgebaut wird. Zuerst programmierst Du einen vollständigen GraphQL-Server mit Apollo, Node.js, Express und Mongoose. Das Beispiel zeigt, wie ein Schema definiert, Resolver geschrieben, Authentifizierung eingebaut und Subscriptions über WebSockets realisiert werden. Anschließend folgt ein Beispiel für einen Apollo Client in React, der Daten vom Server abruft, cacht und automatisch aktualisiert.

1. GraphQL Server mit Apollo und Node.js

// server.js
const { ApolloServer, gql, AuthenticationError, ForbiddenError } = require('apollo-server-express');
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { default: mongoose } = require('mongoose');
const express = require('express');
const http = require('http');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { PubSub } = require('graphql-subscriptions');
const { GraphQLUpload } = require('graphql-upload');
const { GraphQLJSON } = require('graphql-type-json');
const DataLoader = require('dataloader');

// Database Models
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  firstName: String,
  lastName: String,
  avatar: String,
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  isActive: { type: Boolean, default: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const postSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  tags: [String],
  status: { type: String, enum: ['draft', 'published', 'archived'], default: 'draft' },
  featured: { type: Boolean, default: false },
  likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const commentSchema = new mongoose.Schema({
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
  parent: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' },
  likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
const Comment = mongoose.model('Comment', commentSchema);

// GraphQL Schema Definition
const typeDefs = gql`
  scalar Upload
  scalar JSON
  scalar DateTime

  directive @auth(requires: String = "USER") on FIELD_DEFINITION
  directive @admin on FIELD_DEFINITION
  directive @rateLimit(limit: Int, duration: Int) on FIELD_DEFINITION

  type User {
    id: ID!
    username: String!
    email: String!
    firstName: String
    lastName: String
    avatar: String
    role: UserRole!
    isActive: Boolean!
    createdAt: DateTime!
    updatedAt: DateTime!
    posts(limit: Int, offset: Int): [Post!]!
    comments(limit: Int, offset: Int): [Comment!]!
    likedPosts: [Post!]!
    followers: [User!]!
    following: [User!]!
    postCount: Int!
    commentCount: Int!
    followerCount: Int!
    followingCount: Int!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    tags: [String!]!
    status: PostStatus!
    featured: Boolean!
    likes: [User!]!
    comments(limit: Int, offset: Int): [Comment!]!
    likeCount: Int!
    commentCount: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
    isLiked: Boolean
  }

  type Comment {
    id: ID!
    content: String!
    author: User!
    post: Post!
    parent: Comment
    replies: [Comment!]!
    likes: [User!]!
    likeCount: Int!
    replyCount: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
    isLiked: Boolean
  }

  enum UserRole {
    USER
    ADMIN
  }

  enum PostStatus {
    DRAFT
    PUBLISHED
    ARCHIVED
  }

  input UserInput {
    username: String!
    email: String!
    password: String!
    firstName: String
    lastName: String
  }

  input UserUpdateInput {
    username: String
    email: String
    firstName: String
    lastName: String
    avatar: String
  }

  input PostInput {
    title: String!
    content: String!
    tags: [String!]
    status: PostStatus
    featured: Boolean
  }

  input PostUpdateInput {
    title: String
    content: String
    tags: [String!]
    status: PostStatus
    featured: Boolean
  }

  input CommentInput {
    content: String!
    postId: ID!
    parentId: ID
  }

  input CommentUpdateInput {
    content: String
  }

  type AuthPayload {
    token: String!
    user: User!
  }

  type Query {
    # User queries
    me: User @auth
    user(id: ID!): User
    users(limit: Int = 10, offset: Int = 0, search: String): [User!]!
    
    # Post queries
    post(id: ID!): Post
    posts(
      limit: Int = 10
      offset: Int = 0
      status: PostStatus = PUBLISHED
      authorId: ID
      tags: [String]
      search: String
      featured: Boolean
    ): [Post!]!
    trendingPosts(limit: Int = 5): [Post!]!
    
    # Comment queries
    comment(id: ID!): Comment
    comments(postId: ID!, limit: Int = 10, offset: Int = 0): [Comment!]!
    
    # Search queries
    search(query: String!, type: String): SearchResult!
  }

  type Mutation {
    # Authentication mutations
    register(input: UserInput!): AuthPayload!
    login(username: String!, password: String!): AuthPayload!
    refreshToken: String! @auth
    
    # User mutations
    updateProfile(input: UserUpdateInput!): User! @auth
    changePassword(currentPassword: String!, newPassword: String!): Boolean! @auth
    followUser(userId: ID!): Boolean! @auth
    unfollowUser(userId: ID!): Boolean! @auth
    
    # Post mutations
    createPost(input: PostInput!): Post! @auth @rateLimit(limit: 5, duration: 60)
    updatePost(id: ID!, input: PostUpdateInput!): Post! @auth
    deletePost(id: ID!): Boolean! @auth
    likePost(postId: ID!): Post! @auth
    unlikePost(postId: ID!): Post! @auth
    
    # Comment mutations
    createComment(input: CommentInput!): Comment! @auth
    updateComment(id: ID!, input: CommentUpdateInput!): Comment! @auth
    deleteComment(id: ID!): Boolean! @auth
    likeComment(commentId: ID!): Comment! @auth
    unlikeComment(commentId: ID!): Comment! @auth
    
    # File upload mutations
    uploadAvatar(file: Upload!): String! @auth
  }

  type Subscription {
    # Post subscriptions
    postCreated: Post!
    postUpdated(postId: ID): Post!
    postDeleted(postId: ID): ID!
    
    # Comment subscriptions
    commentCreated(postId: ID): Comment!
    commentUpdated(commentId: ID): Comment!
    commentDeleted(commentId: ID): ID!
    
    # User subscriptions
    userOnline(userId: ID): User!
    userOffline(userId: ID): User!
  }

  union SearchResult = User | Post | Comment
`;

// Resolvers
const resolvers = {
  // Custom scalar resolvers
  Upload: GraphQLUpload,
  JSON: GraphQLJSON,
  DateTime: {
    serialize: (value) => new Date(value).toISOString(),
    parseValue: (value) => new Date(value),
    parseLiteral: (ast) => new Date(ast.value)
  },

  // Query resolvers
  Query: {
    me: async (parent, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('You must be logged in');
      }
      return context.user;
    },

    user: async (parent, { id }, context) => {
      try {
        const user = await User.findById(id)
          .populate('followers following')
          .lean();
        
        if (!user || !user.isActive) {
          throw new Error('User not found');
        }
        
        return user;
      } catch (error) {
        throw new Error(`Failed to fetch user: ${error.message}`);
      }
    },

    users: async (parent, { limit = 10, offset = 0, search }, context) => {
      try {
        let query = { isActive: true };
        
        if (search) {
          query.$or = [
            { username: { $regex: search, $options: 'i' } },
            { email: { $regex: search, $options: 'i' } },
            { firstName: { $regex: search, $options: 'i' } },
            { lastName: { $regex: search, $options: 'i' } }
          ];
        }
        
        const users = await User.find(query)
          .populate('followers following')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
        
        return users;
      } catch (error) {
        throw new Error(`Failed to fetch users: ${error.message}`);
      }
    },

    post: async (parent, { id }, context) => {
      try {
        const post = await Post.findById(id)
          .populate('author')
          .populate('comments')
          .lean();
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          post.isLiked = post.likes.some(like => like.toString() === context.user.id);
        }
        
        return post;
      } catch (error) {
        throw new Error(`Failed to fetch post: ${error.message}`);
      }
    },

    posts: async (parent, args, context) => {
      try {
        const {
          limit = 10,
          offset = 0,
          status = 'PUBLISHED',
          authorId,
          tags,
          search,
          featured
        } = args;
        
        let query = { status };
        
        if (authorId) {
          query.author = authorId;
        }
        
        if (tags && tags.length > 0) {
          query.tags = { $in: tags };
        }
        
        if (search) {
          query.$or = [
            { title: { $regex: search, $options: 'i' } },
            { content: { $regex: search, $options: 'i' } }
          ];
        }
        
        if (featured !== undefined) {
          query.featured = featured;
        }
        
        const posts = await Post.find(query)
          .populate('author')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          posts.forEach(post => {
            post.isLiked = post.likes.some(like => like.toString() === context.user.id);
          });
        }
        
        return posts;
      } catch (error) {
        throw new Error(`Failed to fetch posts: ${error.message}`);
      }
    },

    trendingPosts: async (parent, { limit = 5 }, context) => {
      try {
        const posts = await Post.find({ status: 'PUBLISHED' })
          .populate('author')
          .sort({ likes: -1, createdAt: -1 })
          .limit(limit)
          .lean();
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          posts.forEach(post => {
            post.isLiked = post.likes.some(like => like.toString() === context.user.id);
          });
        }
        
        return posts;
      } catch (error) {
        throw new Error(`Failed to fetch trending posts: ${error.message}`);
      }
    },

    comment: async (parent, { id }, context) => {
      try {
        const comment = await Comment.findById(id)
          .populate('author')
          .populate('post')
          .populate('parent')
          .lean();
        
        if (!comment) {
          throw new Error('Comment not found');
        }
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          comment.isLiked = comment.likes.some(like => like.toString() === context.user.id);
        }
        
        return comment;
      } catch (error) {
        throw new Error(`Failed to fetch comment: ${error.message}`);
      }
    },

    comments: async (parent, { postId, limit = 10, offset = 0 }, context) => {
      try {
        const comments = await Comment.find({ post: postId, parent: null })
          .populate('author')
          .populate('replies')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
        
        // Add isLiked field if user is authenticated
        if (context.user) {
          comments.forEach(comment => {
            comment.isLiked = comment.likes.some(like => like.toString() === context.user.id);
          });
        }
        
        return comments;
      } catch (error) {
        throw new Error(`Failed to fetch comments: ${error.message}`);
      }
    },

    search: async (parent, { query, type }, context) => {
      try {
        const searchRegex = { $regex: query, $options: 'i' };
        
        let results = [];
        
        if (!type || type === 'USER') {
          const users = await User.find({
            $or: [
              { username: searchRegex },
              { email: searchRegex },
              { firstName: searchRegex },
              { lastName: searchRegex }
            ],
            isActive: true
          }).limit(5).lean();
          
          results.push(...users);
        }
        
        if (!type || type === 'POST') {
          const posts = await Post.find({
            $or: [
              { title: searchRegex },
              { content: searchRegex },
              { tags: searchRegex }
            ],
            status: 'PUBLISHED'
          }).populate('author').limit(5).lean();
          
          results.push(...posts);
        }
        
        if (!type || type === 'COMMENT') {
          const comments = await Comment.find({
            content: searchRegex
          }).populate('author').populate('post').limit(5).lean();
          
          results.push(...comments);
        }
        
        return results;
      } catch (error) {
        throw new Error(`Search failed: ${error.message}`);
      }
    }
  },

  // Mutation resolvers
  Mutation: {
    register: async (parent, { input }, context) => {
      try {
        const { username, email, password, firstName, lastName } = input;
        
        // Check if user already exists
        const existingUser = await User.findOne({
          $or: [{ username }, { email }]
        });
        
        if (existingUser) {
          throw new Error('User already exists');
        }
        
        // Hash password
        const hashedPassword = await bcrypt.hash(password, 10);
        
        // Create user
        const user = new User({
          username,
          email,
          password: hashedPassword,
          firstName,
          lastName
        });
        
        await user.save();
        
        // Generate JWT token
        const token = jwt.sign(
          { userId: user._id, username: user.username },
          process.env.JWT_SECRET,
          { expiresIn: '7d' }
        );
        
        // Publish user created event
        pubsub.publish('USER_CREATED', {
          userCreated: user
        });
        
        return { token, user };
      } catch (error) {
        throw new Error(`Registration failed: ${error.message}`);
      }
    },

    login: async (parent, { username, password }, context) => {
      try {
        // Find user
        const user = await User.findOne({ username, isActive: true });
        if (!user) {
          throw new Error('Invalid credentials');
        }
        
        // Verify password
        const isValidPassword = await bcrypt.compare(password, user.password);
        if (!isValidPassword) {
          throw new Error('Invalid credentials');
        }
        
        // Generate JWT token
        const token = jwt.sign(
          { userId: user._id, username: user.username },
          process.env.JWT_SECRET,
          { expiresIn: '7d' }
        );
        
        return { token, user };
      } catch (error) {
        throw new Error(`Login failed: ${error.message}`);
      }
    },

    refreshToken: async (parent, args, context) => {
      try {
        if (!context.user) {
          throw new AuthenticationError('Invalid token');
        }
        
        // Generate new token
        const token = jwt.sign(
          { userId: context.user._id, username: context.user.username },
          process.env.JWT_SECRET,
          { expiresIn: '7d' }
        );
        
        return token;
      } catch (error) {
        throw new Error(`Token refresh failed: ${error.message}`);
      }
    },

    updateProfile: async (parent, { input }, context) => {
      try {
        const { username, email, firstName, lastName, avatar } = input;
        
        // Check if username or email is already taken
        if (username || email) {
          const existingUser = await User.findOne({
            _id: { $ne: context.user._id },
            $or: [
              ...(username ? [{ username }] : []),
              ...(email ? [{ email }] : [])
            ]
          });
          
          if (existingUser) {
            throw new Error('Username or email already exists');
          }
        }
        
        // Update user
        const updatedUser = await User.findByIdAndUpdate(
          context.user._id,
          {
            ...(username && { username }),
            ...(email && { email }),
            ...(firstName && { firstName }),
            ...(lastName && { lastName }),
            ...(avatar && { avatar }),
            updatedAt: new Date()
          },
          { new: true }
        ).lean();
        
        return updatedUser;
      } catch (error) {
        throw new Error(`Profile update failed: ${error.message}`);
      }
    },

    changePassword: async (parent, { currentPassword, newPassword }, context) => {
      try {
        // Get user with password
        const user = await User.findById(context.user._id);
        
        // Verify current password
        const isValidPassword = await bcrypt.compare(currentPassword, user.password);
        if (!isValidPassword) {
          throw new Error('Current password is incorrect');
        }
        
        // Hash new password
        const hashedNewPassword = await bcrypt.hash(newPassword, 10);
        
        // Update password
        await User.findByIdAndUpdate(context.user._id, {
          password: hashedNewPassword,
          updatedAt: new Date()
        });
        
        return true;
      } catch (error) {
        throw new Error(`Password change failed: ${error.message}`);
      }
    },

    createPost: async (parent, { input }, context) => {
      try {
        const post = new Post({
          ...input,
          author: context.user._id
        });
        
        await post.save();
        await post.populate('author');
        
        // Publish post created event
        pubsub.publish('POST_CREATED', {
          postCreated: post
        });
        
        return post;
      } catch (error) {
        throw new Error(`Post creation failed: ${error.message}`);
      }
    },

    updatePost: async (parent, { id, input }, context) => {
      try {
        const post = await Post.findById(id);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if user is the author or admin
        if (post.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to update this post');
        }
        
        const updatedPost = await Post.findByIdAndUpdate(
          id,
          {
            ...input,
            updatedAt: new Date()
          },
          { new: true }
        ).populate('author');
        
        // Publish post updated event
        pubsub.publish('POST_UPDATED', {
          postUpdated: updatedPost,
          postId: id
        });
        
        return updatedPost;
      } catch (error) {
        throw new Error(`Post update failed: ${error.message}`);
      }
    },

    deletePost: async (parent, { id }, context) => {
      try {
        const post = await Post.findById(id);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if user is the author or admin
        if (post.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to delete this post');
        }
        
        await Post.findByIdAndDelete(id);
        
        // Publish post deleted event
        pubsub.publish('POST_DELETED', {
          postDeleted: id,
          postId: id
        });
        
        return true;
      } catch (error) {
        throw new Error(`Post deletion failed: ${error.message}`);
      }
    },

    likePost: async (parent, { postId }, context) => {
      try {
        const post = await Post.findById(postId);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if already liked
        if (post.likes.includes(context.user._id)) {
          throw new Error('Post already liked');
        }
        
        // Add like
        post.likes.push(context.user._id);
        await post.save();
        await post.populate('author');
        
        return post;
      } catch (error) {
        throw new Error(`Post like failed: ${error.message}`);
      }
    },

    unlikePost: async (parent, { postId }, context) => {
      try {
        const post = await Post.findById(postId);
        
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Check if not liked
        if (!post.likes.includes(context.user._id)) {
          throw new Error('Post not liked');
        }
        
        // Remove like
        post.likes = post.likes.filter(like => like.toString() !== context.user._id);
        await post.save();
        await post.populate('author');
        
        return post;
      } catch (error) {
        throw new Error(`Post unlike failed: ${error.message}`);
      }
    },

    createComment: async (parent, { input }, context) => {
      try {
        const { content, postId, parentId } = input;
        
        // Verify post exists
        const post = await Post.findById(postId);
        if (!post) {
          throw new Error('Post not found');
        }
        
        // Verify parent comment exists if provided
        if (parentId) {
          const parentComment = await Comment.findById(parentId);
          if (!parentComment) {
            throw new Error('Parent comment not found');
          }
        }
        
        const comment = new Comment({
          content,
          author: context.user._id,
          post: postId,
          parent: parentId
        });
        
        await comment.save();
        await comment.populate('author post parent');
        
        // Publish comment created event
        pubsub.publish('COMMENT_CREATED', {
          commentCreated: comment,
          postId
        });
        
        return comment;
      } catch (error) {
        throw new Error(`Comment creation failed: ${error.message}`);
      }
    },

    updateComment: async (parent, { id, input }, context) => {
      try {
        const comment = await Comment.findById(id);
        
        if (!comment) {
          throw new Error('Comment not found');
        }
        
        // Check if user is the author or admin
        if (comment.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to update this comment');
        }
        
        const updatedComment = await Comment.findByIdAndUpdate(
          id,
          {
            ...input,
            updatedAt: new Date()
          },
          { new: true }
        ).populate('author post parent');
        
        // Publish comment updated event
        pubsub.publish('COMMENT_UPDATED', {
          commentUpdated: updatedComment,
          commentId: id
        });
        
        return updatedComment;
      } catch (error) {
        throw new Error(`Comment update failed: ${error.message}`);
      }
    },

    deleteComment: async (parent, { id }, context) => {
      try {
        const comment = await Comment.findById(id);
        
        if (!comment) {
          throw new Error('Comment not found');
        }
        
        // Check if user is the author or admin
        if (comment.author.toString() !== context.user._id && context.user.role !== 'ADMIN') {
          throw new ForbiddenError('Not authorized to delete this comment');
        }
        
        await Comment.findByIdAndDelete(id);
        
        // Publish comment deleted event
        pubsub.publish('COMMENT_DELETED', {
          commentDeleted: id,
          commentId: id
        });
        
        return true;
      } catch (error) {
        throw new Error(`Comment deletion failed: ${error.message}`);
      }
    }
  },

  // Subscription resolvers
  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
    },

    postUpdated: {
      subscribe: (parent, { postId }) => {
        if (postId) {
          return pubsub.asyncIterator([`POST_UPDATED_${postId}`]);
        }
        return pubsub.asyncIterator(['POST_UPDATED']);
      }
    },

    postDeleted: {
      subscribe: (parent, { postId }) => {
        if (postId) {
          return pubsub.asyncIterator([`POST_DELETED_${postId}`]);
        }
        return pubsub.asyncIterator(['POST_DELETED']);
      }
    },

    commentCreated: {
      subscribe: (parent, { postId }) => {
        if (postId) {
          return pubsub.asyncIterator([`COMMENT_CREATED_${postId}`]);
        }
        return pubsub.asyncIterator(['COMMENT_CREATED']);
      }
    },

    commentUpdated: {
      subscribe: (parent, { commentId }) => {
        if (commentId) {
          return pubsub.asyncIterator([`COMMENT_UPDATED_${commentId}`]);
        }
        return pubsub.asyncIterator(['COMMENT_UPDATED']);
      }
    },

    commentDeleted: {
      subscribe: (parent, { commentId }) => {
        if (commentId) {
          return pubsub.asyncIterator([`COMMENT_DELETED_${commentId}`]);
        }
        return pubsub.asyncIterator(['COMMENT_DELETED']);
      }
    }
  },

  // Field resolvers
  User: {
    posts: async (parent, { limit = 10, offset = 0 }, context) => {
      try {
        return await Post.find({ author: parent._id })
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch user posts: ${error.message}`);
      }
    },

    comments: async (parent, { limit = 10, offset = 0 }, context) => {
      try {
        return await Comment.find({ author: parent._id })
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch user comments: ${error.message}`);
      }
    },

    likedPosts: async (parent, args, context) => {
      try {
        return await Post.find({ likes: parent._id })
          .populate('author')
          .sort({ createdAt: -1 })
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch liked posts: ${error.message}`);
      }
    },

    followers: async (parent, args, context) => {
      try {
        return await User.find({ following: parent._id })
          .sort({ createdAt: -1 })
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch followers: ${error.message}`);
      }
    },

    following: async (parent, args, context) => {
      try {
        return await User.find({ followers: parent._id })
          .sort({ createdAt: -1 })
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch following: ${error.message}`);
      }
    },

    postCount: async (parent, args, context) => {
      try {
        return await Post.countDocuments({ author: parent._id });
      } catch (error) {
        throw new Error(`Failed to count posts: ${error.message}`);
      }
    },

    commentCount: async (parent, args, context) => {
      try {
        return await Comment.countDocuments({ author: parent._id });
      } catch (error) {
        throw new Error(`Failed to count comments: ${error.message}`);
      }
    },

    followerCount: async (parent, args, context) => {
      try {
        return await User.countDocuments({ following: parent._id });
      } catch (error) {
        throw new Error(`Failed to count followers: ${error.message}`);
      }
    },

    followingCount: async (parent, args, context) => {
      try {
        return await User.countDocuments({ followers: parent._id });
      } catch (error) {
        throw new Error(`Failed to count following: ${error.message}`);
      }
    }
  },

  Post: {
    comments: async (parent, { limit = 10, offset = 0 }, context) => {
      try {
        return await Comment.find({ post: parent._id, parent: null })
          .populate('author')
          .sort({ createdAt: -1 })
          .limit(limit)
          .skip(offset)
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch post comments: ${error.message}`);
      }
    },

    likeCount: async (parent, args, context) => {
      return parent.likes.length;
    },

    commentCount: async (parent, args, context) => {
      try {
        return await Comment.countDocuments({ post: parent._id });
      } catch (error) {
        throw new Error(`Failed to count comments: ${error.message}`);
      }
    }
  },

  Comment: {
    replies: async (parent, args, context) => {
      try {
        return await Comment.find({ parent: parent._id })
          .populate('author')
          .sort({ createdAt: -1 })
          .lean();
      } catch (error) {
        throw new Error(`Failed to fetch comment replies: ${error.message}`);
      }
    },

    likeCount: async (parent, args, context) => {
      return parent.likes.length;
    },

    replyCount: async (parent, args, context) => {
      try {
        return await Comment.countDocuments({ parent: parent._id });
      } catch (error) {
        throw new Error(`Failed to count replies: ${error.message}`);
      }
    }
  },

  // Union type resolver
  SearchResult: {
    __resolveType: (obj) => {
      if (obj.username) {
        return 'User';
      }
      if (obj.title) {
        return 'Post';
      }
      if (obj.content && !obj.title) {
        return 'Comment';
      }
      return null;
    }
  }
};

// PubSub for subscriptions
const pubsub = new PubSub();

// Authentication middleware
const authMiddleware = async (req, res, next) => {
  try {
    const token = req.headers.authorization || '';
    
    if (!token) {
      return next();
    }
    
    const decoded = jwt.verify(token.replace('Bearer ', ''), process.env.JWT_SECRET);
    const user = await User.findById(decoded.userId);
    
    if (!user || !user.isActive) {
      return next();
    }
    
    req.user = user;
    next();
  } catch (error) {
    next();
  }
};

// Rate limiting middleware
const rateLimitMap = new Map();

const rateLimitMiddleware = (limit, duration) => {
  return (req, res, next) => {
    const key = req.ip;
    const now = Date.now();
    const windowStart = now - duration * 1000;
    
    if (!rateLimitMap.has(key)) {
      rateLimitMap.set(key, []);
    }
    
    const requests = rateLimitMap.get(key).filter(timestamp => timestamp > windowStart);
    
    if (requests.length >= limit) {
      return res.status(429).json({ error: 'Rate limit exceeded' });
    }
    
    requests.push(now);
    rateLimitMap.set(key, requests);
    
    next();
  };
};

// Apollo Server setup
async function startApolloServer(typeDefs, resolvers) {
  const app = express();
  const httpServer = http.createServer(app);
  
  // Middleware
  app.use(cors());
  app.use(authMiddleware);
  app.use(express.json());
  
  // Data loaders
  const createDataLoaders = () => ({
    userLoader: new DataLoader(async (ids) => {
      const users = await User.find({ _id: { $in: ids } }).lean();
      return ids.map(id => users.find(user => user._id.toString() === id.toString()));
    }),
    postLoader: new DataLoader(async (ids) => {
      const posts = await Post.find({ _id: { $in: ids } }).populate('author').lean();
      return ids.map(id => posts.find(post => post._id.toString() === id.toString()));
    }),
    commentLoader: new DataLoader(async (ids) => {
      const comments = await Comment.find({ _id: { $in: ids } }).populate('author post').lean();
      return ids.map(id => comments.find(comment => comment._id.toString() === id.toString()));
    })
  });
  
  const server = new ApolloServer({
    schema: makeExecutableSchema({
      typeDefs,
      resolvers
    }),
    context: ({ req }) => ({
      user: req.user,
      pubsub,
      dataLoaders: createDataLoaders()
    }),
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer }),
      {
        requestDidStart() {
          return {
            didResolveOperation(requestContext) {
              // Rate limiting based on directives
              const operation = requestContext.request.operation;
              const fieldNodes = operation.selectionSet.selections;
              
              for (const fieldNode of fieldNodes) {
                const directives = fieldNode.directives || [];
                const rateLimitDirective = directives.find(d => d.name.value === 'rateLimit');
                
                if (rateLimitDirective) {
                  const limitArg = rateLimitDirective.arguments.find(arg => arg.name.value === 'limit');
                  const durationArg = rateLimitDirective.arguments.find(arg => arg.name.value === 'duration');
                  
                  if (limitArg && durationArg) {
                    const limit = parseInt(limitArg.value.value);
                    const duration = parseInt(durationArg.value.value);
                    
                    // Apply rate limiting
                    const key = requestContext.request.http.headers.get('x-forwarded-for') || 'unknown';
                    const now = Date.now();
                    const windowStart = now - duration * 1000;
                    
                    if (!rateLimitMap.has(key)) {
                      rateLimitMap.set(key, []);
                    }
                    
                    const requests = rateLimitMap.get(key).filter(timestamp => timestamp > windowStart);
                    
                    if (requests.length >= limit) {
                      throw new Error('Rate limit exceeded');
                    }
                    
                    requests.push(now);
                    rateLimitMap.set(key, requests);
                  }
                }
              }
            }
          };
        }
      }
    ],
    introspection: process.env.NODE_ENV !== 'production',
    csrfPrevention: true
  });
  
  await server.start();
  server.applyMiddleware({ app, path: '/graphql' });
  
  await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
  console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
}

// Connect to database and start server
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/graphql-api', {
  useNewUrlParser: true,
  useUnifiedTopology: true
}).then(() => {
  startApolloServer(typeDefs, resolvers);
}).catch(error => {
  console.error('Database connection error:', error);
});

module.exports = { typeDefs, resolvers };

2. GraphQL Client mit Apollo Client und React

// ApolloClient.js
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { split } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';

// HTTP link
const httpLink = createHttpLink({
  uri: process.env.REACT_APP_GRAPHQL_URI || 'http://localhost:4000/graphql'
});

// WebSocket link for subscriptions
const wsLink = new WebSocketLink({
  uri: process.env.REACT_APP_GRAPHQL_WS_URI || 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
    connectionParams: () => {
      const token = localStorage.getItem('token');
      return {
        authorization: token ? `Bearer ${token}` : ''
      };
    }
  }
});

// Auth link
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');
  
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ''
    }
  };
});

// Error handling link
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
      console.error(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
        extensions
      );
      
      // Handle authentication errors
      if (extensions?.code === 'UNAUTHENTICATED') {
        localStorage.removeItem('token');
        window.location.href = '/login';
      }
      
      // Handle rate limiting
      if (extensions?.code === 'RATE_LIMIT_EXCEEDED') {
        console.warn('Rate limit exceeded. Please try again later.');
      }
    });
  }
  
  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
    
    // Handle network errors
    if (networkError.statusCode === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
  }
});

// Split link for subscriptions
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  from([errorLink, authLink, httpLink])
);

// Apollo Client instance
export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          users: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      },
      User: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          followers: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          following: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      },
      Post: {
        fields: {
          comments: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          likes: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      },
      Comment: {
        fields: {
          replies: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          },
          likes: {
            merge(existing = [], incoming) {
              return [...incoming];
            }
          }
        }
      }
    }
  }),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all',
      notifyOnNetworkStatusChange: true
    },
    query: {
      errorPolicy: 'all'
    }
  }
});

export default client;

// GraphQL Queries
import { gql } from '@apollo/client';

export const GET_ME = gql`
  query GetMe {
    me {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
      postCount
      commentCount
      followerCount
      followingCount
    }
  }
`;

export const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
      postCount
      commentCount
      followerCount
      followingCount
      followers {
        id
        username
        firstName
        lastName
        avatar
      }
      following {
        id
        username
        firstName
        lastName
        avatar
      }
    }
  }
`;

export const GET_USERS = gql`
  GetUsers($limit: Int, $offset: Int, $search: String) {
    users(limit: $limit, offset: $offset, search: $search) {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
      postCount
      commentCount
      followerCount
      followingCount
    }
  }
`;

export const GET_POST = gql`
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const GET_POSTS = gql`
  query GetPosts(
    $limit: Int
    $offset: Int
    $status: PostStatus
    $authorId: ID
    $tags: [String]
    $search: String
    $featured: Boolean
  ) {
    posts(
      limit: $limit
      offset: $offset
      status: $status
      authorId: $authorId
      tags: $tags
      search: $search
      featured: $featured
    ) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const GET_TRENDING_POSTS = gql`
  query GetTrendingPosts($limit: Int) {
    trendingPosts(limit: $limit) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const GET_COMMENTS = gql`
  query GetComments($postId: ID!, $limit: Int, $offset: Int) {
    comments(postId: $postId, limit: $limit, offset: $offset) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      replies {
        id
        content
        createdAt
        updatedAt
        likeCount
        isLiked
        author {
          id
          username
          firstName
          lastName
          avatar
        }
      }
    }
  }
`;

export const SEARCH = gql`
  query Search($query: String!, $type: String) {
    search(query: $query, type: $type) {
      ... on User {
        id
        username
        email
        firstName
        lastName
        avatar
        role
        postCount
        commentCount
        followerCount
        followingCount
      }
      ... on Post {
        id
        title
        content
        status
        featured
        createdAt
        likeCount
        commentCount
        author {
          id
          username
          firstName
          lastName
          avatar
        }
        tags
      }
      ... on Comment {
        id
        content
        createdAt
        likeCount
        author {
          id
          username
          firstName
          lastName
          avatar
        }
        post {
          id
          title
        }
      }
    }
  }
`;

// GraphQL Mutations
export const REGISTER = gql`
  mutation Register($input: UserInput!) {
    register(input: $input) {
      token
      user {
        id
        username
        email
        firstName
        lastName
        avatar
        role
        isActive
        createdAt
        updatedAt
      }
    }
  }
`;

export const LOGIN = gql`
  mutation Login($username: String!, $password: String!) {
    login(username: $username, password: $password) {
      token
      user {
        id
        username
        email
        firstName
        lastName
        avatar
        role
        isActive
        createdAt
        updatedAt
      }
    }
  }
`;

export const UPDATE_PROFILE = gql`
  mutation UpdateProfile($input: UserUpdateInput!) {
    updateProfile(input: $input) {
      id
      username
      email
      firstName
      lastName
      avatar
      role
      isActive
      createdAt
      updatedAt
    }
  }
`;

export const CHANGE_PASSWORD = gql`
  mutation ChangePassword($currentPassword: String!, $newPassword: String!) {
    changePassword(currentPassword: $currentPassword, newPassword: $newPassword)
  }
`;

export const CREATE_POST = gql`
  mutation CreatePost($input: PostInput!) {
    createPost(input: $input) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const UPDATE_POST = gql`
  mutation UpdatePost($id: ID!, $input: PostUpdateInput!) {
    updatePost(id: $id, input: $input) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const DELETE_POST = gql`
  mutation DeletePost($id: ID!) {
    deletePost(id: $id)
  }
`;

export const LIKE_POST = gql`
  mutation LikePost($postId: ID!) {
    likePost(postId: $postId) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const UNLIKE_POST = gql`
  mutation UnlikePost($postId: ID!) {
    unlikePost(postId: $postId) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const CREATE_COMMENT = gql`
  mutation CreateComment($input: CommentInput!) {
    createComment(input: $input) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
      parent {
        id
        content
        author {
          id
          username
          firstName
          lastName
          avatar
        }
      }
    }
  }
`;

export const UPDATE_COMMENT = gql`
  mutation UpdateComment($id: ID!, $input: CommentUpdateInput!) {
    updateComment(id: $id, input: $input) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

export const DELETE_COMMENT = gql`
  mutation DeleteComment($id: ID!) {
    deleteComment(id: $id)
  }
`;

export const LIKE_COMMENT = gql`
  mutation LikeComment($commentId: ID!) {
    likeComment(commentId: $commentId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

export const UNLIKE_COMMENT = gql`
  mutation UnlikeComment($commentId: ID!) {
    unlikeComment(commentId: $commentId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      isLiked
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

// GraphQL Subscriptions
export const POST_CREATED = gql`
  subscription PostCreated {
    postCreated {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const POST_UPDATED = gql`
  subscription PostUpdated($postId: ID) {
    postUpdated(postId: $postId) {
      id
      title
      content
      status
      featured
      createdAt
      updatedAt
      likeCount
      commentCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      tags
    }
  }
`;

export const POST_DELETED = gql`
  subscription PostDeleted($postId: ID) {
    postDeleted(postId: $postId)
  }
`;

export const COMMENT_CREATED = gql`
  subscription CommentCreated($postId: ID) {
    commentCreated(postId: $postId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
      parent {
        id
        content
        author {
          id
          username
          firstName
          lastName
          avatar
        }
      }
    }
  }
`;

export const COMMENT_UPDATED = gql`
  subscription CommentUpdated($commentId: ID) {
    commentUpdated(commentId: $commentId) {
      id
      content
      createdAt
      updatedAt
      likeCount
      replyCount
      author {
        id
        username
        firstName
        lastName
        avatar
      }
      post {
        id
        title
      }
    }
  }
`;

export const COMMENT_DELETED = gql`
  subscription CommentDeleted($commentId: ID) {
    commentDeleted(commentId: $commentId)
  }
`;

// React Components
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useSubscription } from '@apollo/client';
import { GET_POSTS, CREATE_POST, LIKE_POST, UNLIKE_POST, POST_CREATED } from './graphql';

// PostList Component
const PostList = ({ limit = 10, status = 'PUBLISHED' }) => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [offset, setOffset] = useState(0);
  const [hasMore, setHasMore] = useState(true);

  const { data, loading: queryLoading, error: queryError, fetchMore } = useQuery(GET_POSTS, {
    variables: { limit, offset: 0, status },
    notifyOnNetworkStatusChange: true
  });

  const [createPost] = useMutation(CREATE_POST, {
    onCompleted: (data) => {
      setPosts(prev => [data.createPost, ...prev]);
    },
    onError: (error) => {
      console.error('Create post error:', error);
    }
  });

  const [likePost] = useMutation(LIKE_POST);
  const [unlikePost] = useMutation(UNLIKE_POST);

  // Subscription for new posts
  const { data: subscriptionData } = useSubscription(POST_CREATED);

  useEffect(() => {
    if (data) {
      setPosts(data.posts);
      setLoading(false);
    }
  }, [data]);

  useEffect(() => {
    if (queryError) {
      setError(queryError);
      setLoading(false);
    }
  }, [queryError]);

  useEffect(() => {
    if (subscriptionData) {
      setPosts(prev => [subscriptionData.postCreated, ...prev]);
    }
  }, [subscriptionData]);

  const handleLikePost = async (postId, isLiked) => {
    try {
      if (isLiked) {
        await unlikePost({ variables: { postId } });
      } else {
        await likePost({ variables: { postId } });
      }
      
      // Update local state
      setPosts(prev => prev.map(post => 
        post.id === postId 
          ? { ...post, isLiked: !isLiked, likeCount: isLiked ? post.likeCount - 1 : post.likeCount + 1 }
          : post
      ));
    } catch (error) {
      console.error('Like post error:', error);
    }
  };

  const loadMore = () => {
    if (!hasMore || queryLoading) return;

    fetchMore({
      variables: { offset: posts.length },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev;
        
        const newPosts = fetchMoreResult.posts;
        setHasMore(newPosts.length >= limit);
        
        return {
          posts: [...prev.posts, ...newPosts]
        };
      }
    });
  };

  if (loading && posts.length === 0) {
    return <div>Loading posts...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div className="post-list">
      {posts.map(post => (
        <PostItem 
          key={post.id} 
          post={post} 
          onLike={handleLikePost}
        />
      ))}
      
      {hasMore && (
        <button onClick={loadMore} disabled={queryLoading}>
          {queryLoading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
};

// PostItem Component
const PostItem = ({ post, onLike }) => {
  const [expanded, setExpanded] = useState(false);

  const handleLike = () => {
    onLike(post.id, post.isLiked);
  };

  return (
    <div className="post-item">
      <div className="post-header">
        <img 
          src={post.author.avatar || '/default-avatar.png'} 
          alt={post.author.username}
          className="author-avatar"
        />
        <div className="author-info">
          <h4>{post.author.firstName} {post.author.lastName}</h4>
          <p>@{post.author.username}</p>
        </div>
        <div className="post-meta">
          <span className="post-date">
            {new Date(post.createdAt).toLocaleDateString()}
          </span>
          {post.featured && <span className="featured-badge">Featured</span>}
        </div>
      </div>
      
      <div className="post-content">
        <h3>{post.title}</h3>
        <p className={expanded ? 'expanded' : 'collapsed'}>
          {post.content}
        </p>
        {post.content.length > 200 && (
          <button 
            onClick={() => setExpanded(!expanded)}
            className="expand-button"
          >
            {expanded ? 'Show Less' : 'Show More'}
          </button>
        )}
      </div>
      
      <div className="post-tags">
        {post.tags.map(tag => (
          <span key={tag} className="tag">
            #{tag}
          </span>
        ))}
      </div>
      
      <div className="post-actions">
        <button 
          onClick={handleLike}
          className={`like-button ${post.isLiked ? 'liked' : ''}`}
        >
          {post.isLiked ? '❤️' : '🤍'} {post.likeCount}
        </button>
        <button className="comment-button">
          💬 {post.commentCount}
        </button>
        <button className="share-button">
          🔗 Share
        </button>
      </div>
    </div>
  );
};

// CreatePost Component
const CreatePost = () => {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [tags, setTags] = useState('');
  const [status, setStatus] = useState('PUBLISHED');
  const [loading, setLoading] = useState(false);

  const [createPost] = useMutation(CREATE_POST);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);

    try {
      await createPost({
        variables: {
          input: {
            title,
            content,
            tags: tags.split(',').map(tag => tag.trim()).filter(Boolean),
            status
          }
        }
      });

      // Reset form
      setTitle('');
      setContent('');
      setTags('');
      setStatus('PUBLISHED');
    } catch (error) {
      console.error('Create post error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="create-post">
      <h3>Create New Post</h3>
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>Title</label>
          <input
            type="text"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            required
          />
        </div>
        
        <div className="form-group">
          <label>Content</label>
          <textarea
            value={content}
            onChange={(e) => setContent(e.target.value)}
            required
            rows={5}
          />
        </div>
        
        <div className="form-group">
          <label>Tags (comma-separated)</label>
          <input
            type="text"
            value={tags}
            onChange={(e) => setTags(e.target.value)}
            placeholder="javascript, react, graphql"
          />
        </div>
        
        <div className="form-group">
          <label>Status</label>
          <select
            value={status}
            onChange={(e) => setStatus(e.target.value)}
          >
            <option value="DRAFT">Draft</option>
            <option value="PUBLISHED">Published</option>
            <option value="ARCHIVED">Archived</option>
          </select>
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Creating...' : 'Create Post'}
        </button>
      </form>
    </div>
  );
};

export { PostList, PostItem, CreatePost };

GraphQL Schema Design

Type System

graph TD
    A[Schema] --> B[Types]
    A --> C[Queries]
    A --> D[Mutations]
    A --> E[Subscriptions]
    
    B --> F[Scalar Types]
    B --> G[Object Types]
    B --> H[Interface Types]
    B --> I[Union Types]
    B --> J[Enum Types]
    B --> K[Input Types]
    
    C --> L[Data Fetching]
    D --> M[Data Modification]
    E --> N[Real-time Updates]
    
    F --> O[String, Int, Float, Boolean, ID]
    G --> P[User, Post, Comment]
    H --> Q[Node, Entity]
    I --> R[SearchResult]
    J --> S[UserRole, PostStatus]
    K --> T[UserInput, PostInput]

GraphQL vs. REST

Vergleichstabelle

AspektGraphQLREST
Data FetchingExakt benötigte DatenFest definierte Endpunkte
Anzahl RequestsEine Anfrage für alle DatenMehrere Anfragen oft nötig
VersionierungKeine Versionierung nötigURL-Versionierung
Type SafetyStrenge TypisierungLose Typisierung
CachingKomplexer Caching-MechanismusHTTP-Caching einfach
Error HandlingPartielle Fehler möglichHTTP-Statuscodes

Vorteile und Nachteile

Vorteile von GraphQL

  • Effizienz: Nur benötigte Daten abrufen
  • Flexibilität: Eine Abfrage für komplexe Daten
  • Type Safety: Strenge Typisierung verhindert Fehler
  • Evolvability: Schema kann schrittweise erweitert werden
  • Real-time: Subscriptions für Echtzeit-Updates

Nachteile

  • Komplexität: Höhere Lernkurve als REST
  • Caching: Komplexeres Caching als HTTP-Caching
  • File Upload: Datei-Uploads erweitern das Schema
  • Rate Limiting: Komplexere Rate-Limiting-Logik
  • Monitoring: Spezielle Tools erforderlich

FAQ: GraphQL Schema und Kernkomponenten

1. Was ist GraphQL?

GraphQL ist eine Abfragesprache für APIs und eine serverseitige Laufzeitumgebung. Es ermöglicht Clients, genau die Daten anzufordern, die sie benötigen, und definiert ein streng typbasiertes Schema.

2. Ist GraphQL ein Framework?

Nein, GraphQL ist keine fertige Software und kein Framework. Es ist eine Spezifikation und Abfragesprache. Frameworks wie Apollo Server implementieren GraphQL in bestimmten Programmiersprachen.

3. Was ist ein GraphQL Schema?

Das Schema ist der Vertrag zwischen Client und Server. Es beschreibt mit der Schema Definition Language, welche Typen, Queries, Mutations und Subscriptions verfügbar sind.

4. Was ist die Schema Definition Language?

SDL ist eine deklarative Syntax, um GraphQL-Schemata zu definieren. Sie beschreibt Types, Felder, Beziehungen und Operationen auf übersichtliche Weise.

5. Was ist eine Query in GraphQL?

Eine Query ist eine Leseoperation. Sie holt Daten vom Server, ohne sie zu verändern. Der Client kann dabei genau die gewünschten Felder angeben.

6. Was ist eine Mutation in GraphQL?

Eine Mutation ist eine Schreiboperation. Sie verändert Daten auf dem Server, zum Beispiel durch Erstellen, Aktualisieren oder Löschen von Datensätzen.

7. Was ist eine Subscription?

Eine Subscription ist eine Echtzeit-Operation. Der Server informiert den Client über Ereignisse, meist über WebSockets, sobald sich Daten ändern.

8. Was ist ein Resolver?

Ein Resolver ist eine Funktion, die die Daten für ein bestimmtes Feld im Schema liefert. Resolver holen Daten aus Datenbanken oder externen APIs und können Business Logic enthalten.

9. Was sind Skalare Typen in GraphQL?

Skalare Typen sind atomare Datentypen wie String, Int, Float, Boolean und ID. Sie bilden die Blätter des Abfragebaums und enthalten keine weiteren Unterfelder.

10. Was ist ein Object Type?

Ein Object Type ist ein benutzerdefinierter Typ im Schema, der mehrere Felder besitzt. Er repräsentiert eine Entität wie User oder Post und kann mit anderen Typen verknüpft sein.

11. Was ist ein Enum in GraphQL?

Ein Enum ist ein Typ mit einer festen Liste erlaubter Werte. Er wird verwendet, um Felder wie Status, Rollen oder Kategorien typsicher zu definieren.

12. Was ist ein Interface in GraphQL?

Ein Interface definiert eine Menge von Feldern, die von mehreren Typen implementiert werden müssen. Es ermöglicht polymorphe Abfragen über verschiedene Typen hinweg.

13. Was ist eine Union in GraphQL?

Eine Union ist ein Typ, der mehrere Object Types zusammenfasst, ohne gemeinsame Felder zu erzwingen. Bei der Abfrage wird der konkrete Typ mit Inline Fragments unterschieden.

14. Was ist ein Input Type?

Ein Input Type ist ein spezieller Typ für Eingabedaten bei Mutations. Er unterscheidet sich von Object Types, weil er keine Resolver besitzt und nur für Eingaben verwendet wird.

15. Was ist Apollo Server?

Apollo Server ist ein Node.js-Framework für GraphQL-APIs. Es basiert auf einem Schema und Resolvers und bietet Features wie Authentifizierung, Caching und Subscriptions.

16. Was ist Apollo Client?

Apollo Client ist eine JavaScript-Bibliothek für den Zugriff auf GraphQL-APIs. Er cachedt Daten, verwaltet Zustand und aktualisiert UI-Komponenten automatisch bei Datenänderungen.

17. Was ist das N+1-Problem in GraphQL?

Das N+1-Problem entsteht, wenn für eine Liste von Objekten jeweils eine separate Datenbankabfrage ausgeführt wird. Data Loader bündeln diese Anfragen, um die Performance zu verbessern.

18. Was ist ein Data Loader?

Ein Data Loader ist ein Utility, das das Batch-Loading von Daten ermöglicht. Er sammelt Anfragen innerhalb eines kurzen Zeitraums und führt sie in einer einzigen Datenbankabfrage zusammen.

19. Was ist GraphQL Introspection?

Introspection ermöglicht es Clients, das Schema zur Laufzeit abzufragen. Tools wie GraphQL Playground nutzen Introspection, um Autovervollständigung und Dokumentation anzubieten.

20. Was ist GraphQL Playground?

GraphQL Playground ist eine interaktive Entwicklungsumgebung für GraphQL-Queries. Es zeigt das Schema, bietet Autovervollständigung und ermöglicht das Testen von Queries und Mutations.

21. Was ist Type Safety in GraphQL?

Type Safety bedeutet, dass jede Anfrage und jedes Ergebnis gegen das Schema geprüft wird. Das reduziert Laufzeitfehler, da Felder, Typen und Argumente vorab bekannt sind.

22. Was ist Schema Stitching?

Schema Stitching ist die Zusammenführung mehrerer GraphQL-Schemata zu einem einzigen Schema. Es ermöglicht es, verteilte APIs zentral verfügbar zu machen.

23. Was ist Apollo Federation?

Apollo Federation ist eine Architektur, bei der mehrere Microservices jeweils einen Teil des Gesamtschemas bereitstellen. Ein Gateway führt diese Teile zu einem einheitlichen GraphQL-Schema zusammen.

24. Was ist der Unterschied zwischen GraphQL und REST?

GraphQL nutzt ein einziges Endpoint und erlaubt präzise Feldauswahl. REST nutzt mehrere Endpunkte mit festen Ressourcen. GraphQL bietet starke Typisierung, während REST auf HTTP-Methoden und Statuscodes setzt.

25. Wann sollte man GraphQL verwenden?

GraphQL eignet sich besonders für komplexe Datenbeziehungen, mobile Clients mit begrenzter Bandbreite und Anwendungen, bei denen Clients unterschiedliche Datenansichten benötigen. Bei sehr einfachen APIs kann REST ausreichend sein.

Weiter im API Lernpfad

Der nächste Artikel im API Lernpfad behandelt SOAP vs. REST vs. GraphQL — ein Vergleich der drei wichtigsten API-Stile mit Vor- und Nachteilen sowie Einsatzszenarien.

Wichtigste Quellen

  1. https://graphql.org/
  2. https://www.apollographql.com/
  3. https://github.com/graphql/graphql-spec
  4. https://graphql-learn.com/
Zurück zum DEV Blog
Share:

Ähnliche Beiträge