Queries, Mutations, and Resolvers

Module 26: Advanced Backend & API Development

Introduction to GraphQL Operations

In GraphQL, there are three primary types of operations:

  1. Queries: For fetching data (read operations)
  2. Mutations: For modifying data (create, update, delete operations)
  3. Subscriptions: For real-time updates (event-based operations)

Today, we'll focus on queries and mutations, along with resolvers—the functions that fulfill these operations.

Analogy: Restaurant Service Model

Think of GraphQL operations like interactions in a restaurant:

  • Queries: Like ordering from a menu - "I'd like to see these specific dishes"
  • Mutations: Like asking the chef to prepare a special dish or modify an existing one
  • Resolvers: Like the kitchen staff who know how to prepare each item on the menu
  • Schema: The menu that lists what's available and how to order it

GraphQL Queries in Depth

Queries are the most common operations in GraphQL. They allow clients to request exactly the data they need.

Basic Query Structure


query {
  user(id: "123") {
    id
    name
    email
    posts {
      title
      publishedAt
    }
  }
}
      

Key parts of a query:

Query Variables

Variables make queries more reusable by extracting values into a separate dictionary:


query GetUser($userId: ID!) {
  user(id: $userId) {
    id
    name
    email
  }
}

# Variables (sent as JSON):
{
  "userId": "123"
}
      

Benefits of query variables:

Advanced Query Features

Aliases

Aliases let you rename fields in the response:


query {
  activeUser: user(id: "123") {
    id
    fullName: name
    email
  }
  adminUser: user(id: "456") {
    id
    fullName: name
    email
  }
}

# Response:
{
  "data": {
    "activeUser": {
      "id": "123",
      "fullName": "Alice Smith",
      "email": "alice@example.com"
    },
    "adminUser": {
      "id": "456",
      "fullName": "Bob Jones",
      "email": "bob@example.com"
    }
  }
}
      

Fragments

Fragments are reusable units of selection sets:


fragment UserBasics on User {
  id
  name
  email
}

fragment UserPosts on User {
  posts {
    id
    title
    content
  }
}

query {
  regularUser: user(id: "123") {
    ...UserBasics
  }
  contentCreator: user(id: "456") {
    ...UserBasics
    ...UserPosts
  }
}
      

Fragments help with:

Inline Fragments

Inline fragments are useful for querying interfaces and unions:


query {
  search(term: "GraphQL") {
    __typename
    ... on User {
      name
      email
    }
    ... on Post {
      title
      content
    }
    ... on Comment {
      text
      createdAt
    }
  }
}
      

The __typename field is a meta-field that returns the type name of an object.

Directives in Queries

Directives modify how a query is executed. GraphQL includes built-in directives:

@include and @skip


query GetUserDetails($withPosts: Boolean!, $skipEmail: Boolean!) {
  user(id: "123") {
    id
    name
    email @skip(if: $skipEmail)
    posts @include(if: $withPosts) {
      title
      content
    }
  }
}

# Variables:
{
  "withPosts": true,
  "skipEmail": false
}
      

@deprecated

Used in schema definitions to mark fields as deprecated:


type User {
  id: ID!
  name: String!
  email: String!
  username: String @deprecated(reason: "Use email instead")
}
      

Custom directives can be created for specific use cases like formatting, authorization, or caching.

GraphQL Mutations

Mutations are operations that change data on the server. They follow a similar structure to queries but are used for create, update, and delete operations.

Basic Mutation Structure


mutation {
  createUser(input: {
    name: "Jane Doe",
    email: "jane@example.com",
    password: "securepassword"
  }) {
    id
    name
    email
  }
}
      

Key aspects of mutations:

Mutations with Variables


mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}

# Variables:
{
  "input": {
    "name": "Jane Doe",
    "email": "jane@example.com",
    "password": "securepassword"
  }
}
      

Common Mutation Patterns

Create, Update, Delete Pattern


type Mutation {
  # Create
  createPost(input: CreatePostInput!): Post!
  
  # Update
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  
  # Delete
  deletePost(id: ID!): DeletePostPayload!
}

type DeletePostPayload {
  success: Boolean!
  message: String
  id: ID
}
      

Return Types for Mutations

It's often beneficial to create specific payload types for mutations:


type CreateUserPayload {
  user: User
  token: String
  errors: [Error!]
}

type Error {
  path: String!
  message: String!
}

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
}
      

Benefits of payload types:

Real-World Example: E-commerce Order Placement


mutation PlaceOrder($input: PlaceOrderInput!) {
  placeOrder(input: $input) {
    order {
      id
      status
      total
      items {
        product {
          name
          price
        }
        quantity
        subtotal
      }
    }
    paymentUrl
    estimatedDelivery
    errors {
      path
      message
    }
  }
}
        

Resolvers: The Implementation Layer

Resolvers are functions that resolve the values for fields in your schema. They're the connection between the GraphQL schema and your data sources.

graph LR A[Client] -->|GraphQL Query| B[GraphQL Server] B -->|Parse & Validate| C[Execution] C -->|Call Resolvers| D[Resolvers] D -->|Access Data| E[Data Sources] E -->|Return Data| D D -->|Field Values| C C -->|Response| B B -->|JSON Response| A

Resolver Function Signature

A resolver function typically has four arguments:


function resolver(parent, args, context, info) {
  // Implementation
}

// Example resolver for User type's posts field
const resolvers = {
  User: {
    posts: (parent, args, context, info) => {
      // parent: The User object
      // args: Arguments for this field (like limit, offset)
      // context: Shared context object (auth info, db connections)
      // info: Information about the execution state of the query
      
      return context.db.Post.findAll({ 
        where: { authorId: parent.id } 
      });
    }
  }
};
      

Parameters explained:

Resolver Implementation Examples

Query Resolvers


// For a schema like:
// type Query {
//   user(id: ID!): User
//   users(limit: Int, offset: Int): [User!]!
// }

const resolvers = {
  Query: {
    user: (_, args, context) => {
      return context.db.User.findById(args.id);
    },
    
    users: (_, args, context) => {
      const { limit = 10, offset = 0 } = args;
      return context.db.User.findAll({ 
        limit, 
        offset,
        order: [['createdAt', 'DESC']] 
      });
    }
  }
};
      

Mutation Resolvers


// For a schema like:
// type Mutation {
//   createUser(input: CreateUserInput!): User!
//   updateUser(id: ID!, input: UpdateUserInput!): User!
// }

const resolvers = {
  Mutation: {
    createUser: async (_, { input }, context) => {
      // Check if email is already taken
      const existing = await context.db.User.findOne({
        where: { email: input.email }
      });
      
      if (existing) {
        throw new Error('Email already in use');
      }
      
      // Hash password
      const hashedPassword = await bcrypt.hash(input.password, 10);
      
      // Create user
      return context.db.User.create({
        ...input,
        password: hashedPassword
      });
    },
    
    updateUser: async (_, { id, input }, context) => {
      // Check authorization
      if (!context.user || (context.user.id !== id && context.user.role !== 'ADMIN')) {
        throw new Error('Not authorized');
      }
      
      // Find user
      const user = await context.db.User.findById(id);
      if (!user) {
        throw new Error('User not found');
      }
      
      // Update fields
      Object.assign(user, input);
      return user.save();
    }
  }
};
      

Nested Resolvers and Field-Level Resolution

GraphQL executes resolvers for each field in the query, enabling efficient nested data fetching.


// For a schema like:
// type User {
//   id: ID!
//   name: String!
//   posts: [Post!]!
//   totalPosts: Int!
// }
// 
// type Post {
//   id: ID!
//   title: String!
//   content: String!
//   author: User!
//   comments: [Comment!]!
// }

const resolvers = {
  User: {
    posts: (parent, args, context) => {
      return context.db.Post.findAll({
        where: { authorId: parent.id }
      });
    },
    
    totalPosts: async (parent, args, context) => {
      const count = await context.db.Post.count({
        where: { authorId: parent.id }
      });
      return count;
    }
  },
  
  Post: {
    author: (parent, args, context) => {
      return context.db.User.findById(parent.authorId);
    },
    
    comments: (parent, args, context) => {
      return context.db.Comment.findAll({
        where: { postId: parent.id }
      });
    }
  }
};
      

When a client queries:


query {
  user(id: "123") {
    name
    posts {
      title
      comments {
        text
      }
    }
    totalPosts
  }
}
      

GraphQL walks through each field, calling the appropriate resolver:

  1. Resolves user(id: "123") to get the user object
  2. Reads name directly from the user object
  3. Calls the posts resolver to get the user's posts
  4. For each post, reads title directly
  5. For each post, calls the comments resolver
  6. For each comment, reads text directly
  7. Calls the totalPosts resolver

The N+1 Problem and DataLoader

One common issue in GraphQL is the N+1 query problem, where fetching a list of items and a related field for each causes many individual database queries.

The N+1 Problem Example


query {
  posts(limit: 10) {
    title
    author {
      name
    }
  }
}
        

This could result in 11 database queries:

  1. 1 query to get 10 posts
  2. 10 separate queries to get the author for each post

DataLoader is a utility that solves this problem by batching and caching:


const DataLoader = require('dataloader');

// In your server setup
const createLoaders = (db) => ({
  userLoader: new DataLoader(async (userIds) => {
    const users = await db.User.findAll({
      where: { id: { [Op.in]: userIds } }
    });
    
    // Return users in the same order as the ids
    return userIds.map(id => 
      users.find(user => user.id === id) || null
    );
  })
});

// In your server context setup
const context = ({ req }) => ({
  db,
  loaders: createLoaders(db),
  user: req.user
});

// In your resolver
const resolvers = {
  Post: {
    author: (parent, args, context) => {
      return context.loaders.userLoader.load(parent.authorId);
    }
  }
};
      

With DataLoader, the 10 separate author queries would be batched into a single query. This technique is crucial for GraphQL performance.

Error Handling in Resolvers

Properly handling errors is essential for building robust GraphQL APIs. There are several approaches:

Throwing Errors


const resolvers = {
  Mutation: {
    createUser: async (_, { input }, context) => {
      // Validation
      if (!input.email.includes('@')) {
        throw new Error('Invalid email address');
      }
      
      try {
        return await context.db.User.create(input);
      } catch (error) {
        console.error('Failed to create user:', error);
        throw new Error('Failed to create user');
      }
    }
  }
};
      

GraphQL will include these errors in the response:


{
  "data": {
    "createUser": null
  },
  "errors": [
    {
      "message": "Invalid email address",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["createUser"]
    }
  ]
}
      

Using Error Extensions for Rich Errors


const { ApolloError, UserInputError } = require('apollo-server');

const resolvers = {
  Mutation: {
    createUser: async (_, { input }, context) => {
      // Validation
      if (!input.email.includes('@')) {
        throw new UserInputError('Invalid email address', {
          invalidArgs: ['email'],
          code: 'INVALID_EMAIL'
        });
      }
      
      try {
        return await context.db.User.create(input);
      } catch (error) {
        throw new ApolloError('Failed to create user', 'DATABASE_ERROR', {
          dbError: error.message
        });
      }
    }
  }
};
      

Using Result Types with Error Fields


type CreateUserPayload {
  user: User
  errors: [UserError!]
}

type UserError {
  path: String!
  message: String!
  code: String!
}

const resolvers = {
  Mutation: {
    createUser: async (_, { input }) => {
      const errors = [];
      
      if (!input.email.includes('@')) {
        errors.push({ 
          path: 'email', 
          message: 'Invalid email address',
          code: 'INVALID_EMAIL'
        });
      }
      
      if (input.password.length < 8) {
        errors.push({ 
          path: 'password', 
          message: 'Password must be at least 8 characters',
          code: 'PASSWORD_TOO_SHORT'
        });
      }
      
      if (errors.length > 0) {
        return { user: null, errors };
      }
      
      // Create user if no errors
      const user = await db.User.create(input);
      return { user, errors: [] };
    }
  }
};
      

Authentication and Authorization in Resolvers

GraphQL doesn't have a built-in authentication system, but you can implement it in your resolvers:

Context-Based Authentication


// In your context setup
const context = ({ req }) => {
  // Get token from request headers
  const token = req.headers.authorization || '';
  
  // Verify token and get user
  let user = null;
  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      user = getUserById(decoded.userId);
    } catch (error) {
      console.error('Failed to authenticate token:', error);
    }
  }
  
  return { db, user };
};

// In your resolvers
const resolvers = {
  Query: {
    me: (_, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      return context.user;
    },
    
    adminDashboard: (_, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      if (context.user.role !== 'ADMIN') {
        throw new ForbiddenError('Not authorized');
      }
      
      return getDashboardData();
    }
  }
};
      

Using GraphQL Shield for Declarative Permissions

GraphQL Shield is a library that allows for declarative permission rules:


const { shield, rule, and, or, not } = require('graphql-shield');

const isAuthenticated = rule()((_, args, context) => {
  return Boolean(context.user);
});

const isAdmin = rule()((_, args, context) => {
  return context.user && context.user.role === 'ADMIN';
});

const isPostAuthor = rule()(async (_, { id }, context) => {
  const post = await context.db.Post.findById(id);
  return post && post.authorId === context.user.id;
});

const permissions = shield({
  Query: {
    me: isAuthenticated,
    user: isAuthenticated,
    adminDashboard: isAdmin
  },
  Mutation: {
    createPost: isAuthenticated,
    updatePost: and(isAuthenticated, isPostAuthor),
    deletePost: and(isAuthenticated, or(isPostAuthor, isAdmin))
  }
});

// Apply permissions as middleware
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context,
  middleware: [permissions]
});
      

Practical Exercise

Build a GraphQL API for a Blog Platform

In this exercise, you'll implement queries, mutations, and resolvers for a blog platform with the following schema:


type Query {
  post(id: ID!): Post
  posts(filter: PostFilter): [Post!]!
  user(id: ID!): User
  me: User
}

type Mutation {
  createPost(input: CreatePostInput!): PostPayload!
  updatePost(id: ID!, input: UpdatePostInput!): PostPayload!
  deletePost(id: ID!): DeletePostPayload!
  
  register(input: RegisterInput!): AuthPayload!
  login(email: String!, password: String!): AuthPayload!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  createdAt: String!
  updatedAt: String!
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type PostPayload {
  post: Post
  errors: [Error!]
}

type DeletePostPayload {
  success: Boolean!
  errors: [Error!]
}

type AuthPayload {
  token: String
  user: User
  errors: [Error!]
}

type Error {
  path: String!
  message: String!
}

input PostFilter {
  published: Boolean
  authorId: ID
  searchTerm: String
}

input CreatePostInput {
  title: String!
  content: String!
  published: Boolean!
}

input UpdatePostInput {
  title: String
  content: String
  published: Boolean
}

input RegisterInput {
  name: String!
  email: String!
  password: String!
}
      

Tasks:

  1. Implement query resolvers for post, posts, and user
  2. Implement mutation resolvers for createPost and login
  3. Add authentication to the me query and createPost mutation
  4. Implement nested resolvers for User.posts and Post.author
  5. Add proper error handling to all resolvers

For this exercise, you can use in-memory data stores or connect to a database of your choice.

Advanced Resolver Patterns

Computed Fields

You can add computed fields that don't exist in your database:


const resolvers = {
  User: {
    fullName: (parent) => {
      return `${parent.firstName} ${parent.lastName}`;
    },
    
    age: (parent) => {
      if (!parent.birthDate) return null;
      const birthDate = new Date(parent.birthDate);
      const today = new Date();
      let age = today.getFullYear() - birthDate.getFullYear();
      const monthDiff = today.getMonth() - birthDate.getMonth();
      
      if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
        age--;
      }
      
      return age;
    }
  }
};
      

Resolver Composition

You can compose resolvers for reusable logic:


// Higher-order resolver functions
const isAuthenticated = (next) => (root, args, context, info) => {
  if (!context.user) {
    throw new Error('Not authenticated');
  }
  return next(root, args, context, info);
};

const hasRole = (role) => (next) => (root, args, context, info) => {
  if (!context.user || context.user.role !== role) {
    throw new Error(`Must have ${role} role`);
  }
  return next(root, args, context, info);
};

// Using the composed resolvers
const resolvers = {
  Query: {
    me: isAuthenticated((_, args, context) => {
      return context.user;
    }),
    
    adminDashboard: isAuthenticated(
      hasRole('ADMIN')((_, args, context) => {
        return getDashboardData();
      })
    )
  }
};
      

Performance Optimization Techniques

Query Complexity Analysis

Prevent expensive queries by analyzing their complexity:


const { makeExecutableSchema } = require('@graphql-tools/schema');
const { createComplexityRule } = require('graphql-query-complexity');

const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

const complexityRule = createComplexityRule({
  maximumComplexity: 1000,
  variables: {},
  onComplete: (complexity) => {
    console.log('Query complexity:', complexity);
  },
  createError: (max, actual) => {
    return new Error(
      `Query is too complex: ${actual}. Maximum allowed complexity: ${max}`
    );
  },
  estimators: [
    // Default field complexity is 1
    fieldExtensionsEstimator(),
    // Add complexity based on array length
    simpleEstimator({
      defaultComplexity: 1,
      // Higher complexity for fields that return arrays
      listFactor: 10
    })
  ]
});

const server = new ApolloServer({
  schema,
  validationRules: [complexityRule]
});
      

Persisted Queries

Improve network performance by using query IDs instead of the full query text:


// Client setup
const client = new ApolloClient({
  link: createPersistedQueryLink().concat(httpLink),
  cache: new InMemoryCache()
});

// Server setup
const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    cache: new MemcachedCache(
      ['memcached-server-1', 'memcached-server-2', 'memcached-server-3'],
      { retries: 10, retry: 10000 }
    )
  }
});
      

Response Caching

Cache query results to improve performance:


const { ApolloServer } = require('apollo-server-express');
const responseCachePlugin = require('apollo-server-plugin-response-cache');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cache: 'bounded',
  cacheControl: {
    defaultMaxAge: 5, // 5 seconds
    calculateHttpHeaders: true,
  },
  plugins: [responseCachePlugin()]
});

// Add cache hints in resolvers
const resolvers = {
  Query: {
    post: (_, { id }, context, info) => {
      info.cacheControl.setCacheHint({ maxAge: 60 }); // 60 seconds
      return context.db.Post.findById(id);
    }
  }
};
      

Real-World Implementation: Apollo Server

Let's look at a complete Apollo Server implementation for our blog schema:


const { ApolloServer, UserInputError, AuthenticationError } = require('apollo-server');
const { gql } = require('apollo-server');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const DataLoader = require('dataloader');

// Type definitions
const typeDefs = gql`
  type Query {
    post(id: ID!): Post
    posts(filter: PostFilter): [Post!]!
    user(id: ID!): User
    me: User
  }

  type Mutation {
    createPost(input: CreatePostInput!): PostPayload!
    updatePost(id: ID!, input: UpdatePostInput!): PostPayload!
    deletePost(id: ID!): DeletePostPayload!
    
    register(input: RegisterInput!): AuthPayload!
    login(email: String!, password: String!): AuthPayload!
  }

  # Types, inputs, and payloads as defined earlier...
`;

// Create data loaders
const createLoaders = (db) => ({
  userLoader: new DataLoader(async (userIds) => {
    const users = await db.User.findAll({
      where: { id: { [Op.in]: userIds } }
    });
    return userIds.map(id => users.find(user => user.id === id) || null);
  }),
  
  postsByUserLoader: new DataLoader(async (userIds) => {
    const posts = await db.Post.findAll({
      where: { authorId: { [Op.in]: userIds } }
    });
    
    return userIds.map(userId => 
      posts.filter(post => post.authorId === userId)
    );
  })
});

// Resolver functions
const resolvers = {
  Query: {
    post: async (_, { id }, { db }) => {
      return db.Post.findByPk(id);
    },
    
    posts: async (_, { filter = {} }, { db }) => {
      const conditions = {};
      
      if (filter.published !== undefined) {
        conditions.published = filter.published;
      }
      
      if (filter.authorId) {
        conditions.authorId = filter.authorId;
      }
      
      if (filter.searchTerm) {
        conditions[Op.or] = [
          { title: { [Op.iLike]: `%${filter.searchTerm}%` } },
          { content: { [Op.iLike]: `%${filter.searchTerm}%` } }
        ];
      }
      
      return db.Post.findAll({ where: conditions });
    },
    
    user: async (_, { id }, { db }) => {
      return db.User.findByPk(id);
    },
    
    me: async (_, __, { user }) => {
      if (!user) {
        throw new AuthenticationError('Not authenticated');
      }
      return user;
    }
  },
  
  Mutation: {
    register: async (_, { input }, { db }) => {
      const errors = [];
      
      // Validate input
      if (!input.email.includes('@')) {
        errors.push({
          path: 'email',
          message: 'Invalid email format'
        });
      }
      
      if (input.password.length < 8) {
        errors.push({
          path: 'password',
          message: 'Password must be at least 8 characters'
        });
      }
      
      // Check if email is already taken
      const existingUser = await db.User.findOne({
        where: { email: input.email }
      });
      
      if (existingUser) {
        errors.push({
          path: 'email',
          message: 'Email already in use'
        });
      }
      
      if (errors.length > 0) {
        return { user: null, token: null, errors };
      }
      
      // Create user
      const hashedPassword = await bcrypt.hash(input.password, 10);
      const user = await db.User.create({
        ...input,
        password: hashedPassword
      });
      
      // Generate token
      const token = jwt.sign(
        { userId: user.id },
        process.env.JWT_SECRET,
        { expiresIn: '1d' }
      );
      
      return { user, token, errors: [] };
    },
    
    login: async (_, { email, password }, { db }) => {
      // Find user
      const user = await db.User.findOne({ where: { email } });
      
      if (!user) {
        return {
          user: null,
          token: null,
          errors: [{ path: 'email', message: 'No user found with this email' }]
        };
      }
      
      // Check password
      const valid = await bcrypt.compare(password, user.password);
      
      if (!valid) {
        return {
          user: null,
          token: null,
          errors: [{ path: 'password', message: 'Invalid password' }]
        };
      }
      
      // Generate token
      const token = jwt.sign(
        { userId: user.id },
        process.env.JWT_SECRET,
        { expiresIn: '1d' }
      );
      
      return { user, token, errors: [] };
    },
    
    createPost: async (_, { input }, { db, user }) => {
      if (!user) {
        return {
          post: null,
          errors: [{ path: 'auth', message: 'You must be logged in' }]
        };
      }
      
      // Validate input
      const errors = [];
      
      if (!input.title.trim()) {
        errors.push({ path: 'title', message: 'Title cannot be empty' });
      }
      
      if (!input.content.trim()) {
        errors.push({ path: 'content', message: 'Content cannot be empty' });
      }
      
      if (errors.length > 0) {
        return { post: null, errors };
      }
      
      // Create post
      const post = await db.Post.create({
        ...input,
        authorId: user.id
      });
      
      return { post, errors: [] };
    },
    
    updatePost: async (_, { id, input }, { db, user }) => {
      if (!user) {
        return {
          post: null,
          errors: [{ path: 'auth', message: 'You must be logged in' }]
        };
      }
      
      // Find post
      const post = await db.Post.findByPk(id);
      
      if (!post) {
        return {
          post: null,
          errors: [{ path: 'id', message: 'Post not found' }]
        };
      }
      
      // Check ownership
      if (post.authorId !== user.id) {
        return {
          post: null,
          errors: [{ path: 'auth', message: 'Not authorized to update this post' }]
        };
      }
      
      // Update post
      await post.update(input);
      
      return { post, errors: [] };
    },
    
    deletePost: async (_, { id }, { db, user }) => {
      if (!user) {
        return {
          success: false,
          errors: [{ path: 'auth', message: 'You must be logged in' }]
        };
      }
      
      // Find post
      const post = await db.Post.findByPk(id);
      
      if (!post) {
        return {
          success: false,
          errors: [{ path: 'id', message: 'Post not found' }]
        };
      }
      
      // Check ownership
      if (post.authorId !== user.id) {
        return {
          success: false,
          errors: [{ path: 'auth', message: 'Not authorized to delete this post' }]
        };
      }
      
      // Delete post
      await post.destroy();
      
      return { success: true, errors: [] };
    }
  },
  
  // Type resolvers
  User: {
    posts: (parent, _, { loaders }) => {
      return loaders.postsByUserLoader.load(parent.id);
    }
  },
  
  Post: {
    author: (parent, _, { loaders }) => {
      return loaders.userLoader.load(parent.authorId);
    }
  }
};

// Context function
const context = async ({ req }) => {
  // Get token from request headers
  const token = req.headers.authorization?.replace('Bearer ', '') || '';
  
  // Database instance (e.g., Sequelize)
  const db = getDbConnection();
  
  // Create data loaders
  const loaders = createLoaders(db);
  
  // Verify token and get user
  let user = null;
  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      user = await db.User.findByPk(decoded.userId);
    } catch (error) {
      console.error('Failed to authenticate token:', error);
    }
  }
  
  return { db, user, loaders };
};

// Create Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context
});

// Start server
server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
      

This implementation demonstrates:

Conclusion and Key Takeaways

By understanding these concepts, you're now equipped to build powerful, efficient, and maintainable GraphQL APIs.

Additional Resources