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
- Schema Definition: GraphQL type system and SDL
- Query Execution: Query parsing and execution
- Resolver Functions: Data resolution and business logic
- Subscriptions: WebSocket-based real-time updates
- Apollo Server: GraphQL server implementation
- Apollo Client: GraphQL client with caching
- Data Loading: Query optimization
- 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
| Aspect | GraphQL | REST |
|---|---|---|
| Data Fetching | Exactly needed data | Fixed defined endpoints |
| Number of Requests | One request for all data | Multiple requests often necessary |
| Versioning | No versioning required | URL versioning |
| Type Safety | Strict typing | Loose typing |
| Caching | Complex caching mechanism | HTTP caching simple |
| Error Handling | Partial errors possible | HTTP 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
-
What is the difference between Query and Mutation? Queries read data without side effects, while Mutations modify data and can have side effects.
-
Explain GraphQL Resolvers! Resolvers are functions that resolve data for GraphQL fields and contain the business logic.
-
When do you use Subscriptions? Subscriptions are used for real-time updates when clients need to be informed about data changes.
-
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
- https://graphql.org/
- https://www.apollographql.com/
- https://github.com/graphql/graphql-spec
- https://graphql-learn.com/