Skip to content
GraphQL Queries & Resolvers Explained: Arguments, Context & Data Fetching

GraphQL Queries & Resolvers Explained: Arguments, Context & Data Fetching

DodaTech Updated Jun 6, 2026 7 min read

Resolvers are the functions that populate each field in a GraphQL response — they connect the schema to databases, REST APIs, and other data sources.

What You’ll Learn

  • How resolvers map schema fields to data
  • Query arguments and type-specific resolvers
  • Context for authentication and database access
  • The resolver chain and parent arguments
  • DataLoader for batching and caching
  • Pagination with cursors

Why Resolvers Matter

The schema defines what data exists, but resolvers define how to get it. Without resolvers, a GraphQL schema is just documentation. DodaTech’s Durga Antivirus Pro dashboard uses resolvers to fetch device data from Firestore, threat intelligence from an external API, and user profiles from Auth0 — all combined in a single GraphQL response.

    sequenceDiagram
    participant Client
    participant GraphQL
    participant Resolver
    participant DB
    participant REST
    Client->>GraphQL: query { device(id:"1") { name threats { name } } }
    GraphQL->>Resolver: device(parent, {id:"1"}, context)
    Resolver->>DB: SELECT * FROM devices WHERE id = '1'
    DB-->>Resolver: {id:"1", name:"Office-PC", userId:"u1"}
    Resolver-->>GraphQL: {name:"Office-PC"}
    GraphQL->>Resolver: Device.threats(parent={name:"Office-PC", userId:"u1"})
    Resolver->>REST: GET /threats?userId=u1
    REST-->>Resolver: [{name:"Emotet"}]
    Resolver-->>GraphQL: [{name:"Emotet"}]
    GraphQL-->>Client: {data: {device: {name:"Office-PC", threats:[{name:"Emotet"}]}}}
  
Prerequisites: Understanding of GraphQL Types Schema. JavaScript knowledge required.

Resolver Function Signature

Every resolver receives four arguments:

const resolvers = {
  Query: {
    device: (parent, args, context, info) => {
      // parent: result of parent resolver (null for Query root)
      // args: { id: "1" }
      // context: { db, auth, dataSources }
      // info: field selection, path, operation details
      
      return context.db.devices.find(d => d.id === args.id);
    },
  },
};

Resolver Chain

GraphQL resolves queries from top to bottom. Each field’s resolver can return data that becomes the parent for child fields:

const resolvers = {
  Query: {
    threats: () => threatsDb, // Returns all threats
  },
  Threat: {
    // device is a field on Threat type
    device: (parent, args, context) => {
      // parent is the Threat object from the threats resolver
      return context.db.devices.find(d => d.id === parent.deviceId);
    },
  },
};

You might be wondering: “Do I need a resolver for every field?” No — if a field doesn’t have a resolver, GraphQL uses the default resolver which looks for a property with the same name on the parent object.

Context

Context is shared across all resolvers in a request. It’s the right place for authentication, database connections, and DataLoader instances:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    // Verify JWT token from request headers
    const token = req.headers.authorization?.split(' ')[1];
    let user = null;
    
    if (token) {
      try {
        user = await verifyToken(token);
      } catch (e) {
        // Invalid token — user stays null
      }
    }
    
    return {
      user,              // Authenticated user or null
      db,                // Database client
      dataSources: {     // DataLoader instances
        deviceLoader: createDeviceLoader(),
        threatLoader: createThreatLoader(),
      },
    };
  },
});

Resolvers access context via the third argument:

const resolvers = {
  Query: {
    myDevices: (parent, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('Not authenticated');
      }
      return context.db.devices.findByUserId(context.user.id);
    },
  },
};

DataLoader for N+1 Problem

The N+1 problem: fetching 100 devices and their threats makes 1 query for devices + 100 queries for threats = 101 queries.

const DataLoader = require('dataloader');

// Batch function — receives array of keys
async function batchThreatsByDeviceIds(deviceIds) {
  const threats = await db.threats.findByDeviceIds(deviceIds);
  // Must return results in the same order as deviceIds
  return deviceIds.map(id => 
    threats.filter(t => t.deviceId === id)
  );
}

// Create loader in context
const context = {
  threatLoader: new DataLoader(batchThreatsByDeviceIds),
};

// In resolver — uses loader instead of direct query
const resolvers = {
  Device: {
    threats: (parent, args, context) => {
      // Single call per device — DataLoader batches them
      return context.threatLoader.load(parent.id);
    },
  },
};

Now 100 devices + their threats = 1 query for devices + 1 batch query for threats = 2 queries total.

Pagination with Connections

The Relay Connection spec provides cursor-based pagination:

type DeviceConnection {
  edges: [DeviceEdge!]!
  pageInfo: PageInfo!
}

type DeviceEdge {
  cursor: String!
  node: Device!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  devices(first: Int!, after: String): DeviceConnection!
}
const resolvers = {
  Query: {
    devices: async (parent, { first, after }, context) => {
      const afterIndex = after 
        ? Buffer.from(after, 'base64').toString().indexOf(':')
        : -1;
      
      const allDevices = await context.db.devices.findAll();
      const startIndex = afterIndex >= 0 ? afterIndex + 1 : 0;
      const page = allDevices.slice(startIndex, startIndex + first);
      
      return {
        edges: page.map(device => ({
          cursor: Buffer.from(`device:${device.id}`).toString('base64'),
          node: device,
        })),
        pageInfo: {
          hasNextPage: startIndex + first < allDevices.length,
          hasPreviousPage: startIndex > 0,
          startCursor: page[0] ? Buffer.from(`device:${page[0].id}`).toString('base64') : null,
          endCursor: page[page.length - 1] ? Buffer.from(`device:${page[page.length - 1].id}`).toString('base64') : null,
        },
      };
    },
  },
};

Common Mistakes

1. Not Using DataLoader

The N+1 problem causes performance degradation that worsens linearly with data size. Always use DataLoader for related data.

2. Putting Expensive Operations in Context

Context is created once per request. Don’t put expensive initialization there — it runs even for simple queries.

3. Throwing Generic Errors

Use ApolloError subclasses: AuthenticationError, ForbiddenError, UserInputError, ValidationError.

4. Over-fetching in Resolvers

If the client only requests device.name, don’t fetch all threat data in the same resolver. Let child resolvers handle their own data.

Practice Questions

  1. What are the four resolver arguments and their purposes?
  2. How does the resolver chain work for nested queries?
  3. What problem does DataLoader solve?
  4. What is the default resolver behavior?
  5. How does context differ from arguments?

Answers:

  1. parent (parent resolver result), args (query arguments), context (shared state), info (execution details).
  2. Each parent resolver’s return becomes the parent for child field resolvers. GraphQL resolves fields depth-first, so a parent always resolves before its children.
  3. N+1 query problem — fetching parent items and their children separately causes N+1 database queries. DataLoader batches and caches child queries.
  4. If no resolver is defined, GraphQL looks for a property with the field name on the parent object.
  5. Context is shared across all resolvers in a request (auth, DB connections). Arguments are specific to one resolver call.

Challenge: Write resolvers for Durga Antivirus Pro that: (1) fetch a user from the database, (2) batch-load their devices using DataLoader, (3) if the user is an admin, include all devices; otherwise only the user’s own devices, and (4) paginate devices using cursor-based pagination.

FAQ

Can a resolver call other resolvers directly?
: Not directly. You can extract reusable data fetching logic into helper functions that resolvers call. For cross-type relationships, use the resolver chain — the parent resolver returns data that child resolvers reference.
How do I handle file uploads in GraphQL?
: Use the Upload scalar type (built into Apollo Server). Uploads use multipart form data under the hood, not JSON. The resolver receives a file stream that you can pipe to storage.
What is the difference between Query and Mutation resolvers?
: The resolver signature is identical. The difference is semantic: Query resolvers should be side-effect-free and cacheable. Mutation resolvers modify data and are executed sequentially.
How do I test resolvers?
: Write unit tests for resolver functions with mock context and args. Use tools like apollo-server-testing or graphql-tools for integration tests.

Try It Yourself

// A resolver with DataLoader and auth
const resolvers = {
  Query: {
    userDevices: async (parent, args, context) => {
      if (!context.user) throw new AuthenticationError('Login required');
      return context.deviceLoader.load(context.user.id);
    },
  },
  Device: {
    threats: (parent, args, context) => 
      context.threatLoader.load(parent.id),
  },
};

What’s Next

TopicDescription
Mutations GuideCreating, updating, and deleting data
GraphQL ArchitectureServer config, subscriptions, federation
Types & Schema DesignSchema-first design and type system
RESTful APIsCompare resolver patterns with REST endpoints

What’s Next

Congratulations on completing this Graphql Queries Resolvers tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro