GraphQL resolvers can be easy to build, but difficult to get right. A resolver is a function that resolves a value for a field in a schema. There are multiple factors that play a vital role while resolving a particular field. For instance, lets say we have a query like this.

query {
  post: {
     title
     body
     status
     url
     metadata {
        notes
        history
     }
  }
}

When the request comes in, we might want to validate the below points.

  • Check if the user has the right access to read this post.
  • If the user is authorized and is the author of this post, display additional metadata.
  • While returning a valid post, add hostname to the url.

The Problem

Photo by Arron Choi on Unsplash
Photo by Arron Choi on Unsplash

In nodejs world, express is a great library for routing and validations. So we can check all these details at the very begining through middlewares. However express will not have any idea on how to parse the graphql query to validate unless you add another middleware to parse the query and validate. This doesn't feel right because express will eventually handover the request to graphql which will anyways parse the query.

When working with Graphql resolvers, we would want this validation to be close to the resolvers to understand how a request is processed rather than having the logic on express middlewares. The express middleware will be responsible to read the cookie or token from the request, decode, validate and add necessary information to the context. Doing so we will have appropriate information to do validations in the resolver.

post: (parent, args, context) => {
  if (!context.user) return null; // not authorized
  if(context.permission !== "READ") return null; // no read access

  if(context.displayMeta) {
    args = {...args, displayMeta: true}
  }
  // more validations if required
  const post = db.getPost(args);
  return post;
}

But this easily gets bloated with a lot of validation code for complex scenarios. You will see this kind of a validations being written in many resolvers. If you have to change certain business logic, you will have to touch too many parts of your code.


Solution: Chaining resolvers

By creating a chain of resolvers to satisfy individual parts of the overall problem, you will be able to compose an elegant workflow. Keeping business rules isolated helps in managing those rules hassle free and we can handle every concern individually — adding effects declaratively.

Credits: Boopathi - https://www.instagram.com/p/B0wxr-SK5pP/
Credits: Boopathi - https://www.instagram.com/p/B0wxr-SK5pP/

Chaining resolvers can be very useful in such scenarios. Your code can be vastly simplified into this.

createPost: rootResolver
                .createResolver(checkReadAccess)
                .createResolver(getPost)
                .createResolver(addAuthorMeta)
                .createResolver(addHostName);

This helps you in understanding the business logic without too much of implementation details. So, how can we create chainable resolvers ? First we will create the above resolvers and then figure out how can we chain them.

The below resolver will take care of checking the read access for a request. Its small, testable and reusable.

const checkReadAccess = async (parent, args, context) => {
    if(context.PERMISSION.includes("READ") === false ) {
      throw UnauthorizedError({ url: err.fieldName });
    }
    return args;
}

Next let's build the resolver getPost which will take care of fetching a post.

const getPost = async (parent, args, context) => {
    return { post: await db.getPost(args.id) };
}

Next lets build the checkAuthorMeta resolver that will take care of checking if the request has the right permissions for adding the meta data of an author.

const checkAuthorMeta = async (parent, args, context) => {
    if(context.PERMISSION.includes("WRITE") {
      args = {
        ...args,
        post: {
          ...args.post, 
          metadata : await db.getAuthorMeta(args.post.id)
        }
      }
    }
    return args;
}

And lastly, we need the addHostName resolver will take care of adding the hostname to the post url. Later you can add hostname for coverImage, authorImage, etc.

const addHostName = async (parent, args, context) => {
    args = {
        ...args,
        post: {
          ...args.post,
          url: context.host + args.post.url
        }
    }
    return args;
}

Now that we have all the resolvers in place, its time o chain them. We need a funtion which allows us to create a resolver. So lets create a root resolver which will create a new resolver and also attach a special method createResolver to chain with another resolver.

const rootResolver = resolver => {
  const baseResolver = resolver;

  // attach a new method to the base resolver to resolve the next resolver.
  baseResolver.createResolver = childResolver => {
    // The below method will resolve the chained resolver.
    const newResolver = async (root, args, context, err) => {
      const newArgs = await resolver(root, args, context, err);
      return childResolver(root, newArgs, context, err);
    };
    return rootResolver(newResolver);
  };

  return baseResolver;
};

The above function takes a resolver, attaches a method createResolver to it which will resolve the next resolver passed to it along with new arguments. Once we have the above method in place, we will be able to use this method to chain resolvers.


And that is how we can create a nice pipeline for a feature thats is expressive and easy to understand. I personally like to split the resolvers into their concerning effects. It also becomes easier to write schema quickly in a way that improves server side development. These small pure functions also benefit from being reusable and testable. I hope you this post has helped you with some ideas to build your next graphql API.