As you start exploring more and more the world of GraphQL APIs, you may want to get more out of your resolvers. As you decompose your business logic into multiple microservices, all advertising a GraphQL Api, you may encounter a need to query them.

In this article we’ll look at some of the more advanced perks Apollo Server and GraphQL provides out of the box, which would be useful for you.

Resolver’s parameters

Example

Let’s consider this GraphQL schema:

type Query {
    example: Example
}

type Example {
  id: ID
  user: User
}

type User {
  name: String
}

Now you may want to implement its resolver. Resolver functions are passed four arguments: parent, args, context, and info (in that order). Those are the apollo’s convention, you could use any name. Let’s implement one in typescript with all those arguments:

export async function example(
  parent: undefined,
  args: Record<string, any>,
  context: AppContext,
  info: GraphQLResolveInfo
): Promise<Example> {
  return new Example(); // for the example
}

As you can see, you can find a class for each. The parent is undefined, because it’s not a nested object. If we had a resolver on user its parent would be example. The same way we don’t have any arguments like:

type Query {
    exampleWithArgs(arg: String!): Example
}

In this case the argument is a { args: 'argument passed' } which is a Record<string, string>. We don’t have to implement all the arguments in our resolver, they are optional since we may not need them.

Definitions

But for the purpose of this article, let’s review each of them, directly from the ApolloServer’s documentation:

  • parent: The return value of the resolver for this field’s parent (i.e., the previous resolver in the resolver chain). It’s null for mutation. For nested queries, if you know the type of the parent, you should set it.

  • args: An object that contains all GraphQL arguments provided for this field. It’s null if you don’t pass argument, you can create a type for it as we’ve seen in the mutation article.

  • context: An object shared across all resolvers that are executing for a particular operation. We have created an AppContext where this resolver is implemented which holds any information necessary for the resolver. (ie: { _extensionStack: { extensions:[] }, dataSources: { ... } })

  • info: Contains core information specified by GraphQL such as path, root, field name that qre queried and so on. Apollo Server extends it with a cacheControl field.

Usage

Use of parent

Useless for mutation, it becomes relevant when querying custom type’s fields.

For example, when creating a resolver for the field user in the GraphQL type Example the parent will not be undefined. Because it will first hit the Example and retrieves the id before trying to resolve the user.

If you look at a resolver defined like:

const Resolvers = {
  Query: { example, exampleWithArgs },
  Example: {
    user
  }
}

We will have for user a resolver that could be looking like:

export async function user(
  parent: Omit<Example, 'user'>,
  _: undefined, // No arguments here
  context: AppContext,
): Promise<User> {
  return context.findUserFrom(parent.id);
}

The Omit type in typescript allow to create a type minus some keys. In this case we know that the parent is of type Example but the user from this type is not yet resolved and not part of the parent, hence using Omit for clarity.

You can find more about field resolvers in this aritcle about advanced graphql queries.

Use of info

The info is mostly for more advanced use case as you’d normally not need it. But it becomes particularly helpful when you want ahead of time check which fields are being queries so your application can “manually” resolve them.

  • This can happen with multiple nested objects over multiple dataSources or APIs which could grouped to save time when fetching them.
  • Another use case would be when you’re using a GraphQL dataSource to fetch certain data depending on the fields being queried. Knowing in advance what you’ll need to resolve helps you identify what you need or not to fetch.

Let’s take a quick look at what this info field look like, I have removed part of the information, as it can be pretty lengthy. But you should be able to get the gist of it:

const info = {
  fieldName: "example",
  fieldNodes: [],                 // Array of nodes (using fragment creates nested nodes)
  returnType: "Example",
  parentType: "Query",
  path: { key: "example", typename: "Query" },
  schema: {},                     // the whole schema
  fragments: {},                  // the fragment used
  operation: {                    // The actual operation, with the queried fields
    kind: "OperationDefinition",
    operation: "query",
    variableDefinitions: [],
    directives: [],
    selectionSet: {
      kind: "SelectionSet",
      selections: [],
      loc: { start: 0, end: 86 }  // The AST is a string, so it's the character's position
    }
  },
  variableValues: {},
  cacheControl: { cacheHint: { maxAge: 0 } }
}

If you need to “read” the information you might want to check out one of those libraries; Mikhus/graphql-fields-list, robrichard/graphql-fields or graphql-parse-resolve-info

GraphQL functionalities

Out of the box, GraphQL provides directives which are annotations that can be used in the schema like @deprecated or on a query like @skip and @include.

They’re usually for the client side where depending on your use case you may not want to query unnecessary information dynamically.

Use of @skip and @include

The @skip allows you to “skip” parts of the fields in your query. When @skip is true, they won’t be fetched from the server:

query {
  example {
    id
    user @skip(if: true) { name }
  }
}

If you prefer the other way around, you can decide to include or not parts of the fields from your query using the @include directive. When @include is false, the field won’t be fetched from the server:

query {
  example {
    id
    user @include(if: false) { name }
  }
}

Since the directive is placed on the user field, none of the nested fields (like name) will be fetched from those queries. Experiment online or check other use cases.

Fragments

Fragments in GraphQL are parts of a query. When dealing with complex schema it allows to simplify the query notation.</br> Let’s create a fragment for a User in a dedicated “fragments.ts” file with the adequate field:

import { gql } from 'graphql-tag'

export const user = gql`
  fragment user on User {
    name
  }
`

In our case User is an actual GraphQL type and the field name is defined on it. </br> Perfect, now when querying, instead of writing all the fields, we can just import and use our schema such as:

import { user } from "../fragments";

const example: Example = await client.query({
  query: gql`
    ${user}
    query {
      example {
        id
        user { ...user }
      }
    }`
}).then(result => result.data.example)

This way we’re querying the name of the User that is passed through the fragment.

A fragment can also be made out of other fragments, making nested queries much more digest, on more recent version parameters within fragment becomes compatible as well, as the variables gets propagated within them.

The apollo client reads and interprets the fragments thanks to its cache.