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.