Skip to content
IRC-Coding IRC-Coding
GraphQL API development GraphQL schemas Resolvers Subscriptions Apollo GraphQL

GraphQL API Development Basics: Schemas & Resolvers

Learn GraphQL API development fundamentals: schemas, resolvers, subscriptions, and Apollo. Complete guide with practical examples.

S

schutzgeist

2 min read
GraphQL API Development Basics: Schemas & Resolvers

GraphQL API Development Fundamentals: Schemas, Resolvers, Subscriptions & Apollo

This article is a comprehensive introduction to GraphQL API development fundamentals – including schemas, resolvers, subscriptions and Apollo with practical examples.

In a Nutshell

GraphQL is a query language for APIs and a server-side runtime environment for executing these queries with a type-based system for defining data structures.

Compact Technical Description

GraphQL is a query language for APIs that enables clients to request exactly the data they need and retrieve everything in a single request.

Core Components:

GraphQL Schema

  • Types: Define data structures
  • Queries: Data retrieval operations
  • Mutations: Data modification operations
  • Subscriptions: Real-time updates
  • Interfaces: Reusable types
  • Unions: Type groupings

Resolvers

  • Field Resolvers: Resolve data for individual fields
  • Type Resolvers: Type implementations
  • Context: Shared context for resolvers
  • Data Loaders: Avoid N+1 problem
  • Middleware: Authentication and validation

Apollo GraphQL

  • Apollo Server: GraphQL server for Node.js
  • Apollo Client: GraphQL client for web/mobile
  • Apollo Federation: Distributed GraphQL
  • Apollo Studio: GraphQL monitoring
  • Apollo Gateway: GraphQL gateway

Exam-Relevant Key Points

  • GraphQL: Query language for APIs with type-based system
  • Schema: Type definitions for data structures
  • Query: Data retrieval operation with specific fields
  • Mutation: Data modification operations
  • Subscription: Real-time data transmission
  • Resolver: Functions for data resolution
  • Apollo: GraphQL platform for server and client
  • Type System: Strict typing for data
  • IHK-relevant: Modern API development and architecture

Core Components

  1. Schema Definition: GraphQL type system and SDL
  2. Query Execution: Query parsing and execution
  3. Resolver Functions: Data resolution and business logic
  4. Subscriptions: WebSocket-based real-time updates
  5. Apollo Server: GraphQL server implementation
  6. Apollo Client: GraphQL client with caching
  7. Data Loading: Query optimization
  8. Error Handling: Error handling and validation

Practical Examples

1. GraphQL Server with Apollo and 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']);
      }
    }
  },
### 2. GraphQL Client with Apollo Client and React
```jsx
// 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

Comparison Table

AspectGraphQLREST
Data FetchingExactly needed dataFixed defined endpoints
Number of RequestsOne request for all dataMultiple requests often necessary
VersioningNo versioning requiredURL versioning
Type SafetyStrict typingLoose typing
CachingComplex caching mechanismHTTP caching simple
Error HandlingPartial errors possibleHTTP status codes

Advantages and Disadvantages

Advantages of GraphQL

  • Efficiency: Fetch only needed data
  • Flexibility: One query for complex data
  • Type Safety: Strict typing prevents errors
  • Evolvability: Schema can be extended incrementally
  • Real-time: Subscriptions for real-time updates

Disadvantages

  • Complexity: Higher learning curve than REST
  • Caching: More complex caching than HTTP caching
  • File Upload: File uploads extend the schema
  • Rate Limiting: More complex rate limiting logic
  • Monitoring: Special tools required

Common Exam Questions

  1. What is the difference between Query and Mutation? Queries read data without side effects, while Mutations modify data and can have side effects.

  2. Explain GraphQL Resolvers! Resolvers are functions that resolve data for GraphQL fields and contain the business logic.

  3. When do you use Subscriptions? Subscriptions are used for real-time updates when clients need to be informed about data changes.

  4. What are the advantages of GraphQL over REST? GraphQL allows precise data queries in a single request, has strict typing, and does not require versioning.

Most Important Sources

  1. https://graphql.org/
  2. https://www.apollographql.com/
  3. https://github.com/graphql/graphql-spec
  4. https://graphql-learn.com/
Back to Blog
Share: