GraphQL API Development Fundamentals: Schemas, Resolvers, Subscriptions & Apollo
This guide is a comprehensive introduction to GraphQL API development fundamentals – covering schemas, resolvers, subscriptions, and Apollo with practical examples.
In a Nutshell
GraphQL is a query language for APIs and a server-side runtime for executing those queries with a type-based system for defining data structures.
What Exactly Is GraphQL? A Framework, Software, or an Idea?
GraphQL is not a framework and not ready-made software that you install and use immediately. It’s a query language and a specification that precisely describes how clients request data from a server, how the server processes those requests, and what responses come back.
Think of it this way: GraphQL is like SQL for databases, but for APIs. It defines the syntax for queries and the rules for execution. You must implement GraphQL yourself in a programming language or rely on existing libraries and frameworks.
Apollo, for example, is a popular platform and framework that implements GraphQL. It gives you Apollo Server for the backend and Apollo Client for the frontend. Other implementations include Relay, GraphQL Yoga, or urql. GraphQL itself remains independent of any programming language or framework.
In short: GraphQL is an idea and an open standard for APIs that lets clients request exactly the data they actually need.
Technical Overview
GraphQL is a query language for APIs that enables clients to request exactly the data they need and get it all 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: Prevent the 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
Key Concepts at a Glance
- 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 transfer
- Resolver: Functions for data resolution
- Apollo: GraphQL platform for server and client
- Type System: Strong typing for data
- Relevance: Modern API development and architecture
Core Components
-
Schema Definition: GraphQL Type System and SDL The schema is the foundation of every GraphQL API. It uses the Schema Definition Language (SDL) to describe what data structures exist and which operations are allowed. It defines Object Types, Queries, Mutations, Subscriptions, Enums, Interfaces, and Unions. The schema serves as a contract between client and server.
-
Query Execution: Query Parsing and Execution When a client sends a query, it is first parsed and validated against the schema. The execution engine then determines which resolvers are called in what order to return the requested data. This process traverses the GraphQL query tree.
-
Resolver Functions: Data Resolution and Business Logic Each field in a GraphQL schema can have a resolver. Resolvers are functions that provide the actual data for a field. They can load data from databases, external APIs, or other sources and often contain the application’s business logic.
-
Subscriptions: WebSocket-Based Real-Time Updates Subscriptions allow the server to push real-time events to clients. They typically rely on WebSockets. They’re used when clients need to be notified immediately of changes, such as new messages or status updates.
-
Apollo Server: GraphQL Server Implementation Apollo Server is a popular Node.js framework that provides a GraphQL API based on a schema and resolvers. It offers features like authentication, error handling, subscriptions, caching, and plugin integration.
-
Apollo Client: GraphQL Client with Caching Apollo Client is a library for JavaScript/TypeScript applications that lets you execute GraphQL queries, cache data, and automatically update UI components when data changes. The cache reduces unnecessary network requests.
-
Data Loading: Query Optimization Complex GraphQL queries can encounter the N+1 problem, where a separate database query is made for each object. Data loaders batch these requests and minimize the number of database accesses.
-
Error Handling: Error Handling and Validation GraphQL returns a response even when some fields fail. Errors are returned in a separate
errorsarray. Additionally, custom validation, authentication errors, and authorization errors can be handled in the resolvers.
What Is Apollo and Why Should You Use It?
Apollo is a comprehensive platform for GraphQL developed by Apollo GraphQL Inc. It consists of several components that together cover the entire GraphQL workflow: Apollo Server for the backend, Apollo Client for the frontend, and Apollo Studio and Apollo Federation for monitoring and distributed architectures.
Where do you get Apollo?
- Apollo Server and Apollo Client are available as open-source packages via npm.
- Apollo Studio offers a cloud-based interface at https://www.apollographql.com/.
- Documentation and additional tools are available on the official Apollo website.
Who uses Apollo and for what?
- Backend developers use Apollo Server to build GraphQL APIs quickly and with type safety.
- Frontend developers use Apollo Client to fetch and manage data from GraphQL APIs in React, Vue, Angular, or other frameworks.
- DevOps engineers and architects use Apollo Studio and Federation to monitor APIs, document schemas, and distribute them across multiple microservices.
Why should you use Apollo?
- Apollo builds on the official GraphQL standard and is one of the most widely adopted stacks.
- Apollo Server simplifies creating schemas, resolvers, authentication, and subscriptions.
- Apollo Client significantly reduces frontend overhead through intelligent caching and state management.
- Apollo Studio helps identify errors, measure performance, and document your schema.
- Apollo Federation lets you build large APIs from small, independently owned services.
In short: Apollo is the most well-known ecosystem around GraphQL and is especially well-suited when you want to quickly build a production-ready, well-documented, and scalable API.
Practical Examples
The examples below show how to build a GraphQL API in real-world scenarios. You’ll start by building a complete GraphQL server with Apollo, Node.js, Express, and Mongoose. The example demonstrates schema definition, writing resolvers, implementing authentication, and setting up subscriptions over WebSockets. Then comes an example of an Apollo Client in React that fetches data from the server, caches it, and keeps it automatically in sync.
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']);
}
}
},
// Field resolvers
User: {
posts: async (parent, { limit = 10, offset = 0 }, context) => {
try {
return await Post.find({ author: parent._id })
.sort({ createdAt: -1 })
.limit(limit)
.skip(offset)
.lean();
} catch (error) {
throw new Error(`Failed to fetch user posts: ${error.message}`);
}
},
comments: async (parent, { limit = 10, offset = 0 }, context) => {
try {
return await Comment.find({ author: parent._id })
.sort({ createdAt: -1 })
.limit(limit)
.skip(offset)
.lean();
} catch (error) {
throw new Error(`Failed to fetch user comments: ${error.message}`);
}
},
likedPosts: async (parent, args, context) => {
try {
return await Post.find({ likes: parent._id })
.populate('author')
.sort({ createdAt: -1 })
.lean();
} catch (error) {
throw new Error(`Failed to fetch liked posts: ${error.message}`);
}
},
followers: async (parent, args, context) => {
try {
return await User.find({ following: parent._id })
.sort({ createdAt: -1 })
.lean();
} catch (error) {
throw new Error(`Failed to fetch followers: ${error.message}`);
}
},
following: async (parent, args, context) => {
try {
return await User.find({ followers: parent._id })
.sort({ createdAt: -1 })
.lean();
} catch (error) {
throw new Error(`Failed to fetch following:
### 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 | Request exactly what you need | Fixed endpoint definitions |
| Number of Requests | Single request for all data | Multiple requests often required |
| Versioning | No versioning needed | URL-based versioning |
| Type Safety | Strong typing | Loose typing |
| Caching | Complex caching mechanisms | Simple HTTP caching |
| Error Handling | Partial errors possible | HTTP status codes |
Advantages and Disadvantages
Advantages of GraphQL
- Efficiency: Fetch only the data you need
- Flexibility: One query for complex data structures
- Type Safety: Strong typing prevents errors
- Evolvability: Schemas can be extended incrementally
- Real-time: Subscriptions enable live updates
Disadvantages
- Complexity: Steeper learning curve than REST
- Caching: More complex than traditional HTTP caching
- File Uploads: Require schema extensions
- Rate Limiting: More intricate rate-limiting logic
- Monitoring: Specialized tools necessary
FAQ: GraphQL Schema and Core Components
1. What is GraphQL?
2. Is GraphQL a framework?
3. What is a GraphQL schema?
4. What is the Schema Definition Language?
5. What is a Query in GraphQL?
6. What is a Mutation in GraphQL?
7. What is a Subscription?
8. What is a Resolver?
9. What are Scalar Types in GraphQL?
10. What is an Object Type?
11. What is an Enum in GraphQL?
12. What is an Interface in GraphQL?
13. What is a Union in GraphQL?
14. What is an Input Type?
15. What is Apollo Server?
16. What is Apollo Client?
17. What is the N+1 problem in GraphQL?
18. What is a Data Loader?
19. What is GraphQL Introspection?
20. What is GraphQL Playground?
21. What is Type Safety in GraphQL?
22. What is Schema Stitching?
23. What is Apollo Federation?
24. What is the difference between GraphQL and REST?
25. When should you use GraphQL?
Continue Your API Learning Path
The next article in the API learning path covers SOAP vs. REST vs. GraphQL — a comparison of the three major API styles, including their strengths, weaknesses, and use cases.
Key Resources
- https://graphql.org/
- https://www.apollographql.com/
- https://github.com/graphql/graphql-spec
- https://graphql-learn.com/


