diff --git a/website/pages/blog/how-to-write-graphql-resolvers-effectively.mdx b/website/pages/blog/how-to-write-graphql-resolvers-effectively.mdx new file mode 100644 index 000000000..9029a5d91 --- /dev/null +++ b/website/pages/blog/how-to-write-graphql-resolvers-effectively.mdx @@ -0,0 +1,447 @@ +--- +title: How to write GraphQL resolvers effectively +authors: eddeee888 +tags: [graphql, codegen, node, server, typescript] +date: 2024-10-15 +description: + Learn GraphQL concepts like resolver map, resolver chain, mappers, defer resolve and use GraphQL + Code Generator and Server Preset to write resolvers. +image: /blog-assets/the-complete-graphql-scalar-guide/cover.png +thumbnail: /blog-assets/the-complete-graphql-scalar-guide/thumbnail.png +--- + +import { Callout } from '@theguild/components' + +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 + +## 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: + +```graphql +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.movie` resolver returns `null` or `undefined`, the code flow stops here, and the server + returns `movie: null` to the client. +- If `Query.movie` resolver 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.id` and `Movie.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: + +```mermaid +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`, `parent` is always `undefined`. + - For other object-type resolvers like `Movie.id` and `Movie.name`, `parent` is the value returned by parent resolvers like `Query.movie` + - `args`: this is the arguments passed by client operations. In our example query, `Query.movie` resolver would receive `{ id: "1" }` as `args` + - `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.id` and `Actor.id` resolvers must return a non-nullable value that can be coerced into the + `ID` scalar i.e. `string` or `number` values. +- `Movie.name` and `Actor.stageName` resolver must return a non-nullable value that can be coerced + into the `String` scalar i.e. `string`, `boolean` or `number` values. + + + You can learn about GraphQL Scalar, including native Scalar and coercion concept, in this guide + [here](https://the-guild.dev/blog/the-complete-graphql-scalar-guide) + + +It is important to remember that we will encounter runtime errors if the resolver returns unexpected +values. Some common scenarios are: + +- a resolver returns `null` to a non-nullable field +- a resolver returns a value that cannot be coerced into the expected scalar type +- a resolver returns a non-array value to an array field, such as `Movie.actors` + +## How to Implement Resolvers + +### Implementing Resolvers - Or Not + +If an empty resolver map is provided to the GraphQL server, the server still tries to handle +incoming requests. However, it will return `null` for every root-level field in the schema. This +means if a root-level field like `Query.movie`'s return type is non-nullable, we will encounter +runtime error. + +If object type resolvers are omitted, the server will try to return the property of the same name +from `parent`. Here's what `Movie` resolvers may look like if they are omitted: + +```ts +const resolvers = { + Movie: {}, // 👈 Empty `Movie` resolvers object, + Movie: undefined, // 👈 or undefined/omitted `Movie` resolvers object... + + // 👇 ...are the equivalent of the following `Movie` resolvers: + Movie: { + id: parent => parent.id, + name: parent => parent.name, + actors: parent => parent.actors + } +} +``` + +This means if `Query.movie` resolver returns an object with `id`, `name`, and `actors` properties, +we can omit the `Movie` and `Actor` resolvers: + +```ts +const resolvers = { + Query: { + movie: () => { + return { + id: '1', + name: 'Harry Potter and the Half-Blood Prince', + actors: [ + { id: '1', stageName: 'Daniel Radcliffe' }, + { id: '2', stageName: 'Emma Watson' }, + { id: '3', stageName: 'Rupert Grint' } + ] + } + } + } + // 💡 `Movie` and `Actor` resolvers are omitted here but it still works, + // Thanks to default resolvers! +} +``` + +In this very simple example where we are returning static values, this will work. However, in a +real-world scenario where we may fetch data from a database or an external API, we must consider the +response data shape, and the app performance. We will try to simlulate a real-world scenario in the +next section. + +### Implementing Resolvers for Real-World Scenarios + +Below is an object this can be used as an in-memory database: + +```ts +const database = { + movies: { + // Movies table + '1': { + id: '1', + movieName: 'Harry Potter and the Half-Blood Prince' + } + }, + actors: { + // Actors table + '1': { + id: '1', + stageName: 'Daniel Radcliffe' + }, + '2': { + id: '2', + stageName: 'Emma Watson' + }, + '3': { + id: '3', + stageName: 'Rupert Grint' + } + }, + movies_actors: { + // Table containing movie-actor relationship + '1': { + // Movie ID + actorIds: ['1', '2', '3'] + } + } +} +``` + +We usually pass database connection through context, so we might update `Query.movie` to look like +this: + +```ts +const resolvers = { + Query: { + movie: (_, { id }, { database }) => { + const movie = database.movies[id] // 🔄 Database access counter: (1) + + if (!movie) { + return null + } + + return { + id: movie.id, + name: movie.movieName, + actors: (database.movies_actors[id] || []).actorIds // 🔄 (2) + .map(actorId => { + return database.actors[actorId] // 🔄 (3), 🔄 (4), 🔄 (5) + }) + } + } + } +} +``` + +This works, however, there are a few issues: + +- We are accessing the database 5 times every time `Query.movie` runs (Counted using 🔄 emoji in the + previous code snippet). Even if the client may not request for `actors` field, this still happens. + So, we are making unnecessary database calls. This is called **eager resolve**. +- We are mapping `movie.movieName` in the return statement. This is fine here, but our schema may + scale to have multiple fields returning `Movie`, and we may have to repeat the same mapping logic + in multiple resolvers. + +To improve this implementation, we can use **mappers** and **defer resolve** to avoid unnecessary +database calls and reduce code duplication: + +```ts +type MovieMapper = { + // 1. + id: string + movieName: string +} +type ActorMapper = string // 2. + +const resolvers = { + Query: { + movie: (_, { id }, { database }): MovieMapper => { + const movie = database.movies[id] + + if (!movie) { + return null + } + + return movie // 3. + } + }, + Movie: { + name: (parent: MovieMapper) => parent.movieName, // 4. + actors: (parent: MovieMapper, _, { database }): ActorMapper[] => + database.movies_actors[parent.id].actorIds // 5. + // 6. + }, + Actor: { + id: (parent: ActorMapper) => parent, // 7. + stageName: (parent: ActorMapper, _, { database }) => database.actors[parent].stageName // 8. + } +} +``` + +1. We define a `MovieMapper` type to represent the shape of the object returned by `Query.movie` + resolver. +2. Similarly, we define `ActorMapper` to be returned wherever `Actor` type is expected. Note that in + this example, `ActorMapper` is a `string` representing the ID of an actor in this case, instead + of an object. +3. The `MovieMapper` is returned by `Query.movie` resolver, and it becomes the `parent` of `Movie` + resolvers in (4) and (5). +4. Since `MovieMapper` has `movieName` property, but the GraphQL type expects String scalar for + `name` instead. So, we need to return the mapper's `movieName` to the schema's `Movie.name` + field. +5. `MovieMapper` doesn't have `actors` property, so we must find all the related actors from the + database. Here, we expect an array of `ActorMapper` (i.e. an array of `string`) to be returned, + which is the `actorIds` array in the database. This just means the `parent` of each `Actor` + resolver is a string in (7) and (8) +6. We can skip implementing `Movie.id` resolver because by default it passes `MovieMapper.id` + property safely. +7. Because each `ActorMapper` is the ID of an actor, we return the `parent` instead of `parent.id` +8. Similarly, we use `parent` as the ID of the actor to fetch the actor's `stageName` from the + database. + + + By using **mappers** and **defer resolve** techniques, we avoid unnecessary database calls and + reduce code duplication. The next time we need to write a resolver that returns `Movie`, `Actor` + objects, we can simply return the corresponding mapper objects. + + +### Implementing Resolvers with Mappers and Defer Resolve Best Practices + +We saw the benefits of using mappers and defer resolve in the previous section. Here are some +TypeScript best practices to keep in mind when implementing these techniques, failing to enforce +them may result in runtime errors: + +- Mappers MUST be correctly typed as the return type of a resolver and the parent type of the next + resolver. +- Object resolvers MUST be implemented if the expected schema field does not exist in the mapper or, + the mapper's field cannot be coerced into the expected schema type of the same name. + +At a small scale, it is easy to keep track of the types and mappers. However, as our schema grows, +it becomes harder to maintain the typing and remembering which resolvers to implement. This is where +tools like [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and +[Server Preset](https://www.npmjs.com/package/@eddeee888/gcg-typescript-resolver-files) can be used: + +- GraphQL Code Generator: generates TypeScript types for the resolvers, focusing on correct + nullability, array and types. +- Server Preset: generates and wires up resolvers conveniently, and runs analysis to suggests + resolvers that require implementation, reducing the risk of runtime errors. + +To get started, install the required packages: + +```sh npm2yarn +npm i -D @graphql-codegen/cli @eddeee888/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 +``` + +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. + + + 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). + + +## 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.