Telerik blogs

GraphQL resolvers allow a client to specify what data it wants from a server.

In a previous article, we discussed some foundational elements of GraphQL, such as the schema, object types and their relationships. Today, we’ll be diving deeper into another pivotal component of any GraphQL server—GraphQL resolvers.

If we recall, GraphQL is incredibly powerful in allowing a client to specify exactly what data it desires from a server. However, how does the server know how to actually fetch or derive this data? This is where resolvers come into play.

Root Fields

In a GraphQL schema, the primary entry points for client requests are termed root fields. These are fields on the main Query, Mutation and Subscription types. Whenever a client query arrives, it’s the root fields that initially respond.

For example, consider user as a root field on the Query type.

type Query {
  user(id: ID!): User
}

type User {
  name: String!
  email: String!
}

The GraphQL query to fetch user information would look like the following:

{
  user(id: 1) {
    name
    email
  }
}

Resolvers

A GraphQL resolver is essentially a function whose job is to populate the data for a single field in our schema. Whenever a field is executed in a query, the function tied to that field is called which then provides the needed data.

For the user example we have above, to fetch the name and email for a particular user, we’d have a resolver function for the user field. This resolver would interact with our database or any other data source to retrieve the required information.

const resolvers = {
  Query: {
    user: (obj, args, context, info) => {
      // fetch the user data from the database using the id from args
      return database.getUserById(args.id);
    },
  },
};

A GraphQL resolver always receives four positional arguments.

  • obj: The object returned from the resolver on the parent field. For root Query and Mutation object types, this argument is often not used and undefined.
  • args: The arguments provided to the field. For our user query above, the id argument would be available in the args parameter.
  • context: A value provided to every resolver that holds important contextual information. This context could contain information like the currently logged-in user, database connection, etc., and is particularly useful for passing shared data and configurations through resolver chains.
  • info: Used usually only in advanced cases but contains information about the execution state of the query—such as the fieldName, schema, rootValue, etc.

Trivial Resolvers

It’s worth noting that every field in a GraphQL schema is backed by a resolver. If no resolver is explicitly defined for a field, GraphQL libraries would oftentimes utilize a default resolver. To explain this some more, consider the User type in our example GraphQL schema.

type User {
  name: String!
  email: String!
}

If we’ve already retrieved a user object from our database with properties that match our GraphQL object type, and we don’t specify resolvers for name and email, GraphQL will use default resolvers. This means it will look at the parent object (in this case, our user object from the database) for properties with the same name and return them.

// If our user object from the database is like this:
const userFromDatabase = {
  name: "John Doe",
  email: "john@example.com",
};

// GraphQL will resolve the fields as if we had these resolvers:
const resolvers = {
  User: {
    name: (user) => user.name,
    email: (user) => user.email,
  },
};

However, in cases where the fields in our GraphQL schema and the underlying data source aren’t directly mapped or when certain fields require additional computation, custom resolvers become necessary.

Asynchronous Resolvers

Resolvers have the capability to execute asynchronous operations, like database queries or API calls. In these situations, a resolver can yield a promise which eventually settles to an acceptable return value. As an example, suppose we want to fetch a user’s recent purchase history from an external microservice. In our schema, this could look like:

type User {
  name: String!
  email: String!
  recentPurchases: [Purchase!]!
}

type Purchase {
  id: ID!
  item: String!
  price: Float!
}

To obtain this purchase data, we might need to make an asynchronous API call in our resolver function:

const resolvers = {
  User: {
    recentPurchases: async (user, args, context) => {
      // Assuming we have a function to fetch purchases from an API
      const purchases = await api.fetchPurchasesForUser(user.id);
      return purchases;
    },
  },
};

Notice that in this scenario, the resolver function is asynchronous, marked by the async keyword. The function makes use of the await keyword to wait for the API call to complete. This asynchronous pattern ensures that while we’re waiting for data, other operations can continue, making efficient use of the server’s resources.

Field-Level Authorization

One fascinating application of resolvers is to perform field-level authorization. In a GraphQL schema, we can have fields that should only be accessible to certain users based on their roles or permissions. Instead of handling this authorization logic at the controller or route level (as commonly done in RESTful APIs), GraphQL allows us to encapsulate this logic right within the resolver.

Let’s take our User type as an example that now has a new privateNote field:

type User {
  name: String!
  email: String!
  privateNote: String
}

Suppose the privateNote field should only be accessible by the user viewing their own information. We can implement this in the resolver like so:

const resolvers = {
  User: {
    privateNote: (user, args, context) => {
      // Check if the user is authenticated and is the owner
      if (context.currentUser && context.currentUser.id === user.id) {
        return user.privateNote;
      }

      // Return null or throw an error if unauthorized
      return null;
    },
  },
};

In the resolver, we utilize the context argument to determine the currently authenticated user. Depending on the nature of our application, we might choose to return a default value (like null), throw an error or even return a masked value if the request is made by a user who isn’t authenticated.

Though the above approach works well, for real-world production applications, it is often best to delegate authorization logic to the business logic layer. This ensures that the resolver remains clean, focused on data retrieval, and adheres to the principle of separation of concerns.

An example of this can perhaps involve using a hypothetical authorize() middleware function that handles authorization:

const authorize = (resolverFunction) => {
  return (user, args, context, info) => {
    if (!context.currentUser || context.currentUser.id !== user.id) {
      // This could also be a masked value or a custom error
      return null;
    }

    return resolverFunction(user, args, context, info);
  };
};

const resolvers = {
  User: {
    privateNote: authorize((user, args, context) => {
      return user.privateNote;
    }),
  },
};

In the code above, the authorize() function acts as a higher-order function that wraps our original resolver function. It checks the user’s authorization before proceeding to call the actual resolver function. If the user doesn’t have the required permissions, it short-circuits the process and returns a default value (in this case, null).

This approach leads to better modularity and maintainability of the codebase. By externalizing logic like authorization into a middleware function, we can more seamlessly update, expand or replace security mechanisms without the need to dig deep into the core business logic or data-fetching routines.

Wrap-up

Resolvers play a crucial role in GraphQL’s architecture, bridging the gap between the client’s data requests and the actual data sources. Whether resolver functions are used to fetch data, perform computations or transform responses, resolvers ensure that clients get exactly what they ask for.


About the Author

Hassan Djirdeh

Hassan is currently a senior frontend engineer at Doordash. Prior to Doordash, Hassan worked at Instacart and Shopify, where he helped build large production applications at-scale. Hassan is also a published author and course instructor and has helped thousands of students learn in-depth fronted engineering tools like React, Vue, TypeScript and GraphQL. Hassan’s non-work interests range widely and, when not in front of a computer screen, you can find him at the gym, going for walks or running through the six.

Related Posts

Comments

Comments are disabled in preview mode.