🔥 0
0
Lesson 3 of 6 20 min +200 XP

Writing Resolvers & Data Fetching

A schema defines what data exists. Resolvers define how to get it.

Every field in your schema can have a resolver - a function that returns the data for that field.

Schema (What)  +  Resolvers (How)  =  Working GraphQL API

Resolver Basics

A resolver is a function with this signature:

fieldName(parent, args, context, info) {
  // Return the data for this field
}

parent

Result from the parent resolver. For root fields (Query/Mutation), this is often undefined.

args

Arguments passed to the field in the query. e.g., user(id: "123")

context

Shared data across all resolvers - database connections, auth info, etc.

info

Query AST and schema info. Rarely used directly.

Your First Resolvers

Let's build resolvers for a blog API:

// Schema
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
    post(id: ID!): Post
  }
`;

// Resolvers
const resolvers = {
  Query: {
    // Get a single user by ID
    user: (parent, args, context) => {
      return context.db.users.findById(args.id);
    },

    // Get all users
    users: (parent, args, context) => {
      return context.db.users.findAll();
    },

    // Get a single post
    post: (parent, args, context) => {
      return context.db.posts.findById(args.id);
    }
  }
};

Default Resolvers

GraphQL has a default resolver that returns parent[fieldName]. This means you don't need resolvers for every field:

// If your database returns:
{
  id: "123",
  name: "Sarah Chen",
  email: "sarah@example.com"
}

// GraphQL's default resolver handles these fields automatically!
// You don't need to write:
// User: {
//   id: (parent) => parent.id,
//   name: (parent) => parent.name,
//   email: (parent) => parent.email
// }
Pro Tip

Only write custom resolvers when you need to transform data or fetch from a different source. Let default resolvers do the heavy lifting.

Nested Resolvers

The power of GraphQL comes from nested data. Here's how to resolve relationships:

const resolvers = {
  Query: {
    user: (_, args, context) => context.db.users.findById(args.id),
    post: (_, args, context) => context.db.posts.findById(args.id)
  },

  // Resolvers for User type fields
  User: {
    // Fetch posts for this user
    posts: (parent, args, context) => {
      // parent is the User object
      return context.db.posts.findByAuthorId(parent.id);
    }
  },

  // Resolvers for Post type fields
  Post: {
    // Fetch author for this post
    author: (parent, args, context) => {
      // parent is the Post object
      return context.db.users.findById(parent.authorId);
    }
  }
};

Execution Flow

When this query runs:

query {
  user(id: "123") {
    name
    posts {
      title
      author {
        name
      }
    }
  }
}
1. Query.user(id: "123")
Returns: { id: "123", name: "Sarah" }
2. User.name (default resolver)
Returns: "Sarah"
3. User.posts(parent: { id: "123", name: "Sarah" })
Returns: [{ id: "p1", title: "GraphQL Guide", authorId: "123" }]
4. Post.title (default resolver)
Returns: "GraphQL Guide"
5. Post.author(parent: { id: "p1", title: "...", authorId: "123" })
Returns: { id: "123", name: "Sarah" }

Mutation Resolvers

Mutations modify data and return results:

const resolvers = {
  Mutation: {
    createPost: async (_, args, context) => {
      // Verify user is authenticated
      if (!context.user) {
        throw new Error("Authentication required");
      }

      // Create the post
      const post = await context.db.posts.create({
        title: args.input.title,
        content: args.input.content,
        authorId: context.user.id,
        published: args.input.published || false
      });

      return post;
    },

    updatePost: async (_, args, context) => {
      const post = await context.db.posts.findById(args.id);

      // Check ownership
      if (post.authorId !== context.user.id) {
        throw new Error("Not authorized");
      }

      return context.db.posts.update(args.id, args.input);
    },

    deletePost: async (_, args, context) => {
      const post = await context.db.posts.findById(args.id);

      if (post.authorId !== context.user.id) {
        throw new Error("Not authorized");
      }

      await context.db.posts.delete(args.id);
      return true;
    }
  }
};

Setting Up Context

Context is created per-request and passed to all resolvers:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // Get auth token from headers
    const token = req.headers.authorization || "";

    // Verify and decode the token
    const user = verifyToken(token);

    return {
      // Database connections
      db: {
        users: new UserService(),
        posts: new PostService()
      },

      // Current user (null if not authenticated)
      user,

      // Other shared data
      loaders: createLoaders()  // For DataLoader (next lesson!)
    };
  }
});

Error Handling

Throw errors in resolvers - GraphQL handles them gracefully:

const resolvers = {
  Query: {
    user: async (_, args, context) => {
      const user = await context.db.users.findById(args.id);

      if (!user) {
        throw new Error(`User with ID ${args.id} not found`);
      }

      return user;
    }
  }
};

Response with error:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User with ID 999 not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"]
    }
  ]
}

Custom Error Classes

import { GraphQLError } from 'graphql';

// Custom errors with codes
class NotFoundError extends GraphQLError {
  constructor(message) {
    super(message, {
      extensions: { code: 'NOT_FOUND' }
    });
  }
}

class AuthenticationError extends GraphQLError {
  constructor(message = 'Authentication required') {
    super(message, {
      extensions: { code: 'UNAUTHENTICATED' }
    });
  }
}

// Usage in resolver
const resolvers = {
  Query: {
    user: async (_, args, context) => {
      if (!context.user) {
        throw new AuthenticationError();
      }

      const user = await context.db.users.findById(args.id);

      if (!user) {
        throw new NotFoundError(`User ${args.id} not found`);
      }

      return user;
    }
  }
};

Complete Server Example

import { ApolloServer, gql } from 'apollo-server';

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

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

  type Mutation {
    createPost(title: String!, content: String!): Post!
  }
`;

// In-memory data for demo
const users = [
  { id: "1", name: "Sarah", email: "sarah@example.com" },
  { id: "2", name: "Alex", email: "alex@example.com" }
];

const posts = [
  { id: "p1", title: "GraphQL Intro", content: "...", authorId: "1" },
  { id: "p2", title: "REST vs GraphQL", content: "...", authorId: "1" }
];

const resolvers = {
  Query: {
    me: (_, __, context) => context.user,
    user: (_, args) => users.find(u => u.id === args.id),
    posts: () => posts
  },

  Mutation: {
    createPost: (_, args, context) => {
      const post = {
        id: `p${posts.length + 1}`,
        title: args.title,
        content: args.content,
        authorId: context.user?.id || "1"
      };
      posts.push(post);
      return post;
    }
  },

  User: {
    posts: (parent) => posts.filter(p => p.authorId === parent.id)
  },

  Post: {
    author: (parent) => users.find(u => u.id === parent.authorId)
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    user: users[0]  // Simulated logged-in user
  })
});

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

Resolver Patterns

Pattern When to Use
Thin resolvers Call service layer, don't put business logic in resolvers
Default resolvers Let GraphQL handle simple field mapping
Context for shared state DB connections, auth, loaders
Async resolvers Return promises for DB/API calls

Key Takeaways

  • Resolvers fetch data - They're the "how" to the schema's "what"
  • Four arguments - parent, args, context, info
  • Default resolvers work - Only write custom ones when needed
  • Context is per-request - Use it for auth, DB, shared data
  • Nested resolution - Each field resolver receives its parent's result

Next up: The N+1 Problem - Learn about the most common GraphQL performance pitfall and how to fix it with DataLoader.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What are the four arguments passed to every resolver function?

2

When is a field resolver called?

3

What's the best practice for database access in resolvers?