Understanding the Apollo Default Resolver

According to documentation for Apollo’s GraphQL-tools:

You don’t need to specify resolvers for every type in your schema. If you don’t specify a resolver, GraphQL.js falls back to a default one…

The documentation goes on to state that the default resolver will look for a property on the parent object with the field name that’s being resolved. If that property is not a function, the value of the property is returned. But if the property does contain a function, then the default resolver calls it, and “passes the query arguments into that function.”

It wasn’t clear to me exactly what this meant, so I did some experimenting.

Normal Resolver Signature

The documentation states that every resolver function accepts four positional arguments:


  fieldName(obj, args, context, info) { result }

You can read the Apollo documentation for an explanation of each argument.

When a resolver is explicitly specified for a type/field, that resolver will always be used. And even if a parent resolver provides a value for a field, it will be overridden by the result of the explicitly specified resolver.


const resolverMap = {
  Query: {
    getAuthor(obj, args, context, info) {
      return { name: "Frank" };
    },
  },
  Author: {
    name(obj, args, context, info) {
      console.log("Provided name", obj.name);
      return "Hank";
    }
  },
};

The above resolvers would resolve to “Hank” for the author’s name (but the console.log would print out “Frank”).

Default Resolver

However, if a resolver is not specified for a field, then the default resolver is used. As described above, if the parent object has a property with the relevant field name, that value will be returned. Modifying the previous example:


  const resolverMap = {
    Query: {
      getAuthor(obj, args, context, info) {
        return { name: "Frank" };
      },
    },
  };

This code results in the name of the author always resolving to “Frank.”

If the property with the relevant field name contains a function, the default resolver will call that function. This function has nearly the same signature as a normal resolver, minus the first obj argument. Continuing with the same example:


  function nameFunc(args, context, info) {
    return "Sal";
  }

  const resolverMap = {
    Query: {
      getAuthor(obj, args, context, info) {
        return { name: nameFunc };
      },
    },
  };

In this configuration, the author’s name will always be “Sal.”

Note that the args argument (in the first position) are any arguments specified for that particular field. So for this query:


query {
  getAuthor(id: 5){
    name(foo: "bar")
  }
}

The nameFunc above would have an args value of { foo: "bar" } passed into it.

The context and info arguments are the same as for a normal resolver.

Conclusion

I’ve definitely gotten confused by the different ways a resolver can be specified and what can be passed into a resolver function. Hopefully, this post can help clarify things a bit.