This article was published on Monday, November 4, 2024 by
Resolvers are the fundamental building blocks of a GraphQL server. To build a robust and scalable
GraphQL server, we must understand how to write GraphQL resolvers effectively. In this blog post, we
will explore:
- how resolvers work
- concepts such as resolver map, resolver chain, defer resolve and mappers
- tools and best practices
Glossary
- Resolver map: An object containing resolvers that match the types and fields in the GraphQL
schema. - Resolver chain: The order of resolvers execution when the GraphQL server handles a request.
- Mapper: The shape of the data returned by a resolver to become the
parentparameter of the
next resolver in the resolver chain. - Defer resolve: A technique to avoid unnecessary resolver execution of a field early in the
resolver chain.
What Are Resolvers?
In a GraphQL server, a resolver is a function that "resolves" a value which means doing arbitrary
combination of logic to return a value. For example:
- returning a value statically
- fetching data from a database or an external API to return a value
- executing a complex business logic to return a value
Each field in a GraphQL schema has an optional corresponding resolver function. When a client
queries a field, the server executes the resolver function to resolve the field.
Given this example schema:
```graphql filename="src/graphql/schema.graphql"
type Query {
movie(id: ID!): Movie
}
type Movie {
id: ID!
name: String!
actors: [Actor!]!
}
type Actor {
id: ID!
stageName: String!
}
We can write a **resolver map** like this:
```ts filename="src/graphql/resolvers.ts"
const resolvers = {
Query: {
movie: () => {} // `Query.movie` resolver
},
Movie: {
id: () => {}, // `Movie.id` resolver
name: () => {}, // `Movie.name` resolver
actors: () => {} // `Movie.actors` resolver
},
Actor: {
id: () => {}, // `Actor.id` resolver
stageName: () => {} // `Actor.stageName` resolver
}
}
We will discuss how the code flows through resolvers when the server handles a request in the next
section.
Code Flow and Resolver Chain
Using the same schema, we may send a query like this:
query Movie {
movie(id: "1") {
id
name
actors {
id
stageName
}
}
}
Once the server receives this query, it starts at Query.movie resolver, and since it returns a
nullable Movie object type, two scenarios can happen:
- If
Query.movieresolver returnsnullorundefined, the code flow stops here, and the server
returnsmovie: nullto the client. - If
Query.movieresolver returns anything else (e.g. objects, class instances, number, non-null
falsy values, etc.), the code flow continues. Whatever being returned - usually called
mapper - will be the first argument of the Movie resolvers i.e.Movie.idandMovie.name
resolvers.
This process repeats itself until a GraphQL scalar field needs to be resolved. The order of the
resolvers execution is called the resolver chain. For the example request, the resolver chain
may look like this:
flowchart LR
A[Query.movie] --> B(Movie.id)
A[Query.movie] --> C(Movie.name)
A[Query.movie] --> D(Movie.actors)
D --> E(Actor.id)
D --> F(Actor.stageName)
There are four positonal arguments of a resolver function:
-
parent: the value returned by the parent resolver.
- For root-level resolvers like
Query.movie,parentis alwaysundefined. - For other object-type resolvers like
Movie.idandMovie.name,parentis the value returned by parent resolvers likeQuery.movie
- For root-level resolvers like
-
args: this is the arguments passed by client operations. In our example query,Query.movieresolver would receive{ id: "1" }asargs
-
context: An object passed through the resolver chain. It is useful for passing information between resolvers, such as authentication information, database connection, etc. -
info: An object containing information about the operation, such as operation AST, path to the resolver, etc.
We must return a value that can be handled by the scalars. In our example:
-
Movie.idandActor.idresolvers must return a non-nullable value that can be coerced into the
IDscalar i.e.stringornumbervalues. -
Movie.nameandActor.stageNameresolver must return a non-nullable value that can be coerced
into theStringscalar i.e.string,booleanornumbervalues.
You can learn about GraphQL Scalar, including native Scalar and coercion concept, in this guide
and
/gcg-typescript-resolver-files
Next, create a `codegen.ts` file at the root of your project:
```ts filename="codegen.ts"
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files'
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'src/graphql/schema.graphql',
generates: {
'src/graphql': defineConfig({
resolverGeneration: 'minimal'
})
}
}
export default config
Then, add mappers into schema.mappers.ts file, in the same directory as schema.graphql:
```ts filename="src/graphql/schema.mappers.ts"
type MovieMapper = {
id: string
movieName: string
}
type ActorMapper = string
<Callout type="info" emoji="💡">
Server Preset automatically detects and wires up mappers if they follow the convention:
1. The mappers are declared in a file in the same directory as the schema source file, and has the file name ending with `.mappers.ts` segment instead of the schema source file's extension. For example:
* If your schema file is `schema.graphql`, the mappers file is `schema.mappers.ts`.
* If your schema file is `schema.graphql.ts`, the mappers file is `schema.graphql.mappers.ts`.
2. The mapper type names are in the format of `<TypeName>Mapper` where `<TypeName>` is the GraphQL schema name. For example:
* If the schema type is `Movie`, then the mapper type is `MovieMapper`.
</Callout>
Finally, run codegen to generate resolvers:
```sh npm2yarn
npm run graphql-codegen
We will see generated resolver files in src/graphql directory:
```ts filename="src/graphql/resolvers/Query/movie.ts"
import type { QueryResolvers } from './../../types.generated'
export const movie: NonNullable = async (_parent, _arg, _ctx) => {
/* Implement Query.movie resolver logic here */
}
```ts filename="src/graphql/resolvers/Movie.ts"
import type { MovieResolvers } from './../types.generated'
export const Movie: MovieResolvers = {
/* Implement Movie resolver logic here */
actors: async (_parent, _arg, _ctx) => {
/* Movie.actors resolver is required because Movie.actors exists but MovieMapper.actors does not */
},
name: async (_parent, _arg, _ctx) => {
/* Movie.name resolver is required because Movie.name exists but MovieMapper.name does not */
}
}
```ts filename="src/graphql/resolvers/Actor.ts"
import type { ActorResolvers } from './../types.generated'
export const Actor: ActorResolvers = {
/* Implement Actor resolver logic here /
id: async (_parent, _arg, _ctx) => {
/ Actor.id resolver is required because Actor.id exists but ActorMapper.id does not /
},
stageName: async (_parent, _arg, _ctx) => {
/ Actor.stageName resolver is required because Actor.stageName exists but ActorMapper.stageName does not */
}
}
By providing mappers, codegen is smart enough to understand that we want to defer resolve, and we
need to write logic for `Movie.actors`, `Movie.name`, `Actor.id` and `Actor.stageName` resolvers to
ensure we don't encounter runtime errors.
<Callout type="info" emoji="💡">
Learn how to set up GraphQL Code Generator and Server Preset for GraphQL Yoga and Apollo Server in
this guide
[here](https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset).
</Callout>
## Summary
In this article, we have explored how resolvers work in a GraphQL server resolver code flow and
**resolver chain**, and how to write resolvers effectively using **mappers** and **defer resolve**
techniques. Finally, we add GraphQL Code Generator and Server Preset to automatically generate
resolvers and their types to ensure strong type-safety and reduce runtime errors.
SOCIAL SHARE CARD GENERATOR