GraphQL
GraphQL cheat sheet with query syntax, mutations, subscriptions, schema types, resolvers, and API design patterns with code examples.
15 min read
graphqlapiqueriesmutationssubscriptionsapolloschemaresolvers
Sign in to mark items as known and track your progress.
Sign inSetup & Basics
GraphQL Server Setup
📄 Codejavascript
// Install dependencies
npm install @apollo/server @as-integrations/express4 graphql graphql-tag express cors
// Basic GraphQL server with Apollo
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@as-integrations/express4');
const { gql } = require('graphql-tag');
const express = require('express');
const cors = require('cors');
// Type definitions (Schema)
const typeDefs = gql`
type Query {
hello: String
users: [User!]!
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
`;
// Resolvers
const resolvers = {
Query: {
hello: () => 'Hello World!',
users: () => users,
user: (parent, args) => users.find(u => u.id === args.id)
},
User: {
posts: (parent) => posts.filter(p => p.authorId === parent.id)
}
};
// Create server
async function startServer() {
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
app.use('/graphql', cors(), express.json(), expressMiddleware(server));
app.listen(4000, () => {
console.log('Server running at http://localhost:4000/graphql');
});
}
startServer();💡 GraphQL is a query language and runtime for APIs
⚡ Single endpoint typically at /graphql
📌 Strongly typed with schema definition
🟢 Works with any backend language or database
Schema Definition Language (SDL)
📄 Codegraphql
# Scalar types
scalar Date
scalar JSON
# Enums
enum Role {
ADMIN
USER
GUEST
}
# Object types
type User {
id: ID! # Non-nullable ID
name: String! # Required string
email: String # Optional string
age: Int
isActive: Boolean!
role: Role!
createdAt: Date!
metadata: JSON
posts: [Post!]! # Non-null list of non-null Posts
friends: [User] # Nullable list of nullable Users
}
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User! # Relationship
comments: [Comment!]!
tags: [String!]!
}
# Input types for mutations
input CreateUserInput {
name: String!
email: String!
password: String!
role: Role = USER # Default value
}
input UpdateUserInput {
name: String
email: String
isActive: Boolean
}
# Interfaces
interface Node {
id: ID!
}
# Union types
union SearchResult = User | Post | Comment
# Root types
type Query {
# User queries
users(limit: Int = 10, offset: Int = 0): [User!]!
user(id: ID!): User
currentUser: User
# Post queries
posts(published: Boolean): [Post!]!
post(id: ID!): Post
# Search
search(query: String!): [SearchResult!]!
}
type Mutation {
# User mutations
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
deleteUser(id: ID!): Boolean!
# Post mutations
createPost(title: String!, content: String!): Post!
publishPost(id: ID!): Post
}
type Subscription {
# Real-time updates
userAdded: User!
postPublished: Post!
commentAdded(postId: ID!): Comment!
}💡 Schema defines API structure and types
⚡ ! means non-nullable, [] means list
📌 Scalar types: Int, Float, String, Boolean, ID
🔥 Custom scalars for dates, JSON, etc.
Queries
Basic queries
graphql
# Simple query
query {
hello
}
# Query with fields selection
query {
users {
id
name
email
}
}
# Query with arguments
query {
user(id: "123") {
name
email
age
}
}
# Nested queries
query {
user(id: "123") {
id
name
posts {
id
title
comments {
id
content
author {
name
}
}
}
}
}💡 Request exactly what you need
⚡ Single request for nested data
📌 No over-fetching or under-fetching
🟢 Predictable results matching query shape
Query variables and directives
graphql
# Query with variables
query GetPosts($limit: Int!, $offset: Int = 0, $published: Boolean) {
posts(limit: $limit, offset: $offset, published: $published) {
id
title
content
author {
name
}
}
}
# Variables object (sent separately)
{
"limit": 10,
"offset": 20,
"published": true
}
# Directives
query GetUser($userId: ID!, $includeDetails: Boolean!, $skipPosts: Boolean!) {
user(id: $userId) {
id
name
# Include directive
email @include(if: $includeDetails)
phone @include(if: $includeDetails)
address @include(if: $includeDetails)
# Skip directive
posts @skip(if: $skipPosts) {
id
title
}
}
}
# Custom directives
query {
users {
id
name
email @lowercase
createdAt @formatDate(format: "MM/DD/YYYY")
password @deprecated(reason: "Use auth service")
}
}💡 Variables make queries reusable
⚡ Directives modify query execution
📌 @include and @skip for conditional fields
🔥 Custom directives for advanced features
Mutations
Basic mutations
graphql
# Simple mutation
mutation {
createUser(input: {
name: "John Doe"
email: "john@example.com"
password: "secure123"
}) {
id
name
email
}
}
# Mutation with variables
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
published
author {
name
}
}
}
# Update mutation
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
updatedAt
}
}
# Delete mutation
mutation DeletePost($id: ID!) {
deletePost(id: $id) {
success
message
}
}💡 Mutations modify server-side data
⚡ Return updated data after mutation
📌 Use input types for complex arguments
🟢 Can include queries in response
Optimistic updates and error handling
📄 Codejavascript
// Apollo Client mutation with optimistic response
import { gql, useMutation } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
createdAt
author {
id
name
}
}
}
`;
function CreatePostForm() {
const [createPost, { data, loading, error }] = useMutation(CREATE_POST, {
// Optimistic response
optimisticResponse: {
createPost: {
__typename: 'Post',
id: 'temp-id',
title: formData.title,
content: formData.content,
createdAt: new Date().toISOString(),
author: {
__typename: 'User',
id: currentUser.id,
name: currentUser.name
}
}
},
// Update cache after mutation
update(cache, { data: { createPost } }) {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: createPost,
fragment: gql`
fragment NewPost on Post {
id
title
content
createdAt
author {
id
name
}
}
`
});
return [...existingPosts, newPostRef];
}
}
});
},
// Error handling
onError(error) {
console.error('Mutation error:', error);
toast.error('Failed to create post');
},
// Success handling
onCompleted(data) {
toast.success('Post created successfully');
resetForm();
}
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
await createPost({
variables: {
title: formData.title,
content: formData.content
}
});
} catch (err) {
// Error is handled by onError
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}💡 Optimistic UI updates for better UX
⚡ Rollback on error automatically
📌 Error handling with try-catch
🔥 Cache updates after mutations
Subscriptions
Real-time subscriptions
javascript
// Server-side subscription setup
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
// Type definitions
const typeDefs = gql`
type Subscription {
postAdded: Post!
commentAdded(postId: ID!): Comment!
userTyping(chatId: ID!): TypingEvent!
}
type TypingEvent {
userId: ID!
userName: String!
isTyping: Boolean!
}
`;
// Resolvers
const resolvers = {
Mutation: {
createPost: async (parent, args, context) => {
const post = await db.post.create({ data: args });
// Publish event
pubsub.publish('POST_ADDED', { postAdded: post });
return post;
},
addComment: async (parent, { postId, content }, context) => {
const comment = await db.comment.create({
data: { postId, content, userId: context.user.id }
});
// Publish to specific channel
pubsub.publish(`COMMENT_ADDED_${postId}`, {
commentAdded: comment
});
return comment;
}
},
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator(['POST_ADDED'])
},
commentAdded: {
subscribe: (parent, { postId }) => {
return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
}
},
userTyping: {
subscribe: withFilter(
() => pubsub.asyncIterator(['USER_TYPING']),
(payload, variables) => {
// Only send to users in the same chat
return payload.chatId === variables.chatId;
}
)
}
}
};💡 Real-time updates via WebSocket
⚡ Server pushes updates to clients
📌 Use for live features like chat
🔥 Efficient for selective updates
Resolvers
Resolver structure and context
javascript
// Resolver function signature
const resolvers = {
Query: {
// (parent, args, context, info) => result
user: async (parent, args, context, info) => {
// parent: Result from parent resolver
// args: Arguments passed to field
// context: Shared context (auth, db, etc.)
// info: Query AST and execution info
// Check authentication
if (!context.user) {
throw new Error('Not authenticated');
}
// Fetch from database
return await context.db.user.findUnique({
where: { id: args.id }
});
}
}
};
// Context setup - context lives on the integration (Apollo Server 4+)
const server = new ApolloServer({ typeDefs, resolvers });
app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
context: async ({ req }) => {
// Get auth token
const token = req.headers.authorization || '';
// Verify user
const user = await verifyToken(token);
// Return context object
return {
user,
db: prisma,
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI()
},
req,
pubsub
};
},
}));💡 Resolvers fetch data for schema fields
⚡ Four arguments: parent, args, context, info
📌 Context shares data across resolvers
🟢 Can be sync or async functions
DataLoader and N+1 problem
📄 Codejavascript
// N+1 Problem Example (BAD)
const resolvers = {
Query: {
posts: async (parent, args, { db }) => {
return await db.post.findMany(); // 1 query
}
},
Post: {
author: async (parent, args, { db }) => {
// This runs once per post! (N queries)
return await db.user.findUnique({
where: { id: parent.authorId }
});
}
}
};
// Result: 1 + N queries (if 100 posts, 101 queries!)
// Solution with DataLoader (GOOD)
const DataLoader = require('dataloader');
// Create loaders
function createLoaders(db) {
return {
user: new DataLoader(async (userIds) => {
// Batch load all users in one query
const users = await db.user.findMany({
where: { id: { in: userIds } }
});
// Map back to original order
const userMap = new Map();
users.forEach(user => userMap.set(user.id, user));
return userIds.map(id => userMap.get(id));
}),
postsByUser: new DataLoader(async (userIds) => {
// Batch load all posts
const posts = await db.post.findMany({
where: { authorId: { in: userIds } }
});
// Group by user
const postMap = new Map();
userIds.forEach(id => postMap.set(id, []));
posts.forEach(post => {
const userPosts = postMap.get(post.authorId);
userPosts.push(post);
});
return userIds.map(id => postMap.get(id));
})
};
}
// Use in context - loaders attach via the integration (Apollo Server 4+)
const server = new ApolloServer({ typeDefs, resolvers });
app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
context: async ({ req }) => ({
db,
loaders: createLoaders(db) // New instance per request
})
}));
// Use in resolvers
const resolvers = {
Post: {
author: async (parent, args, { loaders }) => {
// Automatically batched!
return await loaders.user.load(parent.authorId);
}
},
User: {
posts: async (parent, args, { loaders }) => {
// Batched and cached
return await loaders.postsByUser.load(parent.id);
}
}
};
// Result: Just 2 queries total!💡 DataLoader batches and caches requests
⚡ Solves N+1 query problem
📌 Automatic request deduplication
🔥 Per-request caching
Authentication & Authorization
Authentication patterns
javascript
// Authentication setup
const jwt = require('jsonwebtoken');
const { GraphQLError } = require('graphql');
// Context with authentication
const server = new ApolloServer({ typeDefs, resolvers });
// Context moves to the integration function (Apollo Server 4+)
app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
context: async ({ req }) => {
// Get token from header
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from database
user = await db.user.findUnique({
where: { id: decoded.userId }
});
} catch (err) {
// Invalid token, but don't throw here
console.error('Invalid token:', err);
}
}
return { user, db };
},
}));
// Login mutation
const resolvers = {
Mutation: {
login: async (parent, { email, password }, { db }) => {
// Find user
const user = await db.user.findUnique({ where: { email } });
if (!user) {
throw new GraphQLError('Invalid credentials', { extensions: { code: 'UNAUTHENTICATED' } });
}
// Check password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
throw new GraphQLError('Invalid credentials', { extensions: { code: 'UNAUTHENTICATED' } });
}
// Generate token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return {
token,
user
};
},
register: async (parent, { input }, { db }) => {
// Check if user exists
const existing = await db.user.findUnique({
where: { email: input.email }
});
if (existing) {
throw new Error('User already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(input.password, 10);
// Create user
const user = await db.user.create({
data: {
...input,
password: hashedPassword
}
});
// Generate token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET
);
return { token, user };
}
}
};💡 Use context for auth state
⚡ JWT tokens common for GraphQL
📌 Check auth in resolvers or middleware
🔐 Never expose sensitive fields
Error Handling
Error types and handling
javascript
// Apollo Server error types
const { GraphQLError } = require('graphql');
// Custom error classes
class NotFoundError extends GraphQLError {
constructor(message) {
super(message, { extensions: { code: 'NOT_FOUND' } });
}
}
class ConflictError extends GraphQLError {
constructor(message) {
super(message, { extensions: { code: 'CONFLICT' } });
}
}
// Use in resolvers
const resolvers = {
Query: {
user: async (parent, { id }, { db }) => {
const user = await db.user.findUnique({ where: { id } });
if (!user) {
throw new NotFoundError(`User with ID ${id} not found`);
}
return user;
}
},
Mutation: {
createUser: async (parent, { input }, { db }) => {
// Validation
if (!input.email.includes('@')) {
throw new GraphQLError('Invalid email format', {
extensions: { code: 'BAD_USER_INPUT', field: 'email', value: input.email }
});
}
// Check uniqueness
const existing = await db.user.findUnique({
where: { email: input.email }
});
if (existing) {
throw new ConflictError('Email already in use');
}
try {
return await db.user.create({ data: input });
} catch (error) {
// Database error
console.error('Database error:', error);
throw new GraphQLError('Failed to create user', { extensions: { code: 'DATABASE_ERROR' } });
}
}
}
};💡 GraphQL returns 200 OK even with errors
⚡ Errors in errors array, data can be partial
📌 Use custom error classes for clarity
🔥 Format errors for production
Performance Optimization
Query optimization techniques
javascript
// Query depth limiting
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5) // Max depth of 5
]
});
// Query complexity analysis
const {
createComplexityLimitRule
} = require('graphql-validation-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query cost:', cost),
scalarCost: 1,
objectCost: 2,
listFactor: 10,
introspectionCost: 1000,
})
]
});
// Caching with Redis
const Redis = require('ioredis');
const redis = new Redis();
const resolvers = {
Query: {
popularPosts: async (parent, args, context) => {
const cacheKey = 'popular_posts';
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const posts = await db.post.findMany({
where: { published: true },
orderBy: { views: 'desc' },
take: 10
});
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(posts));
return posts;
}
}
};💡 Use DataLoader for batching
⚡ Implement query depth limiting
📌 Add query complexity analysis
🔥 Cache with Redis or in-memory
Testing
Testing GraphQL APIs
javascript
// Unit testing resolvers
const { expect } = require('chai');
const sinon = require('sinon');
describe('User Resolvers', () => {
let db;
beforeEach(() => {
// Mock database
db = {
user: {
findUnique: sinon.stub(),
findMany: sinon.stub(),
create: sinon.stub(),
update: sinon.stub()
}
};
});
describe('Query.user', () => {
it('should return user by id', async () => {
const mockUser = { id: '1', name: 'John' };
db.user.findUnique.resolves(mockUser);
const result = await resolvers.Query.user(
null,
{ id: '1' },
{ db }
);
expect(result).to.deep.equal(mockUser);
expect(db.user.findUnique.calledWith({
where: { id: '1' }
})).to.be.true;
});
it('should throw error if user not found', async () => {
db.user.findUnique.resolves(null);
try {
await resolvers.Query.user(null, { id: '999' }, { db });
expect.fail('Should have thrown');
} catch (error) {
expect(error.message).to.include('not found');
}
});
});
});
// Integration testing with test server
const { ApolloServer } = require('@apollo/server');
const { gql } = require('graphql-tag');
describe('GraphQL Integration Tests', () => {
let server;
beforeEach(() => {
server = new ApolloServer({ typeDefs, resolvers });
});
it('should fetch users', async () => {
const GET_USERS = gql`
query {
users {
id
name
email
}
}
`;
const res = await server.executeOperation(
{ query: GET_USERS },
{ contextValue: { db: mockDb, user: { id: '1', role: 'USER' } } }
);
// Results live under res.body.singleResult
expect(res.body.singleResult.errors).to.be.undefined;
expect(res.body.singleResult.data.users).to.be.an('array');
});
it('should create post', async () => {
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
}
}
`;
const res = await server.executeOperation(
{
query: CREATE_POST,
variables: {
title: 'Test Post',
content: 'Test content'
}
},
{ contextValue: { db: mockDb, user: { id: '1', role: 'USER' } } }
);
expect(res.body.singleResult.errors).to.be.undefined;
expect(res.body.singleResult.data.createPost).to.include({
title: 'Test Post',
content: 'Test content'
});
});
});💡 Test resolvers in isolation
⚡ Use test server for integration tests
📌 Mock data sources and context
🟢 Test both success and error cases
Best Practices
Schema design principles
graphql
# GOOD Schema Design
type User {
id: ID!
name: String!
email: String!
profile: UserProfile!
posts(
limit: Int = 10
offset: Int = 0
orderBy: PostOrderBy = CREATED_AT_DESC
): PostConnection!
}
# Use connections for pagination
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Use input types for complex arguments
input CreatePostInput {
title: String!
content: String!
tags: [String!]
published: Boolean = false
}
# Use enums for fixed values
enum PostOrderBy {
CREATED_AT_ASC
CREATED_AT_DESC
TITLE_ASC
TITLE_DESC
POPULARITY
}
# Deprecate instead of removing
type Post {
id: ID!
title: String!
content: String!
body: String! @deprecated(reason: "Use 'content' instead")
author: User!
tags: [String!]!
}
# BAD Schema Design (Avoid these)
type User {
userId: String! # Use 'id: ID!' instead
user_name: String! # Use camelCase: userName
getPosts: [Post] # Don't use verbs, just 'posts'
postsArray: [Post] # Redundant 'Array' suffix
}
# Too nested (BAD)
type User {
posts: [Post!]!
comments: [Comment!]!
replies: [Reply!]!
reactions: [Reaction!]! # Too deep!
}💡 Design schema for clients, not database
⚡ Use clear, consistent naming
📌 Avoid deep nesting (max 3-4 levels)
🎯 Version via field deprecation, not versions
Security best practices
📄 Codejavascript
// Security configuration
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@as-integrations/express4');
const { GraphQLError } = require('graphql');
const rateLimit = require('express-rate-limit');
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-cost-analysis');
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max 100 requests
message: 'Too many requests'
});
app.use('/graphql', limiter);
// Apollo Server with security
const server = new ApolloServer({
typeDefs,
resolvers,
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
// Validation rules
validationRules: [
// Limit query depth
depthLimit(7),
// Limit query complexity
costAnalysis({
maximumCost: 1000,
defaultCost: 1,
variables: {},
scalarCost: 1,
objectCost: 2,
listFactor: 10,
introspectionCost: 1000,
enforceIntrospectionCost: true,
onComplete: (cost) => {
console.log('Query cost:', cost);
}
})
],
// Format errors (hide sensitive info)
formatError: (err) => {
// Log full error server-side
console.error(err);
// Don't leak internal errors
if (err.message.includes('Database')) {
return new Error('Internal server error');
}
return err;
},
});
// Context moves to the integration (Apollo Server 4+)
app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
context: async ({ req }) => {
// CSRF protection
const csrfToken = req.headers['x-csrf-token'];
if (req.method === 'POST' && !csrfToken) {
throw new Error('CSRF token missing');
}
// Rate limiting per user
const ip = req.ip;
const userLimiter = getUserRateLimiter(ip);
if (!userLimiter.check()) {
throw new Error('Rate limit exceeded');
}
return { req };
},
}));
// Input validation
const validator = require('validator');
const resolvers = {
Mutation: {
createUser: async (parent, { input }) => {
// Validate email
if (!validator.isEmail(input.email)) {
throw new GraphQLError('Invalid email', { extensions: { code: 'BAD_USER_INPUT' } });
}
// Validate and sanitize
const sanitized = {
name: validator.escape(input.name),
email: validator.normalizeEmail(input.email),
bio: validator.escape(input.bio || '')
};
// Check for SQL injection attempts
if (containsSQLInjection(input)) {
throw new Error('Invalid input');
}
return createUser(sanitized);
}
}
};
// Timeout protection
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart() {
return {
willSendResponse(requestContext) {
// Timeout after 30 seconds
setTimeout(() => {
if (!requestContext.response.http.body) {
throw new Error('Request timeout');
}
}, 30000);
}
};
}
}
]
});🔐 Always validate and sanitize inputs
⚡ Implement rate limiting
📌 Disable introspection in production
🔥 Use query depth and complexity limits