Better Type Safety for your GraphQL resolvers with GraphQL Codegen

This article was published on 2020-05-17 by Dotan Simha @ The Guild Blog

If you use TypeScript to write your GraphQL schema implementation, you'll love the integration with GraphQL Codegen and typescript-resolvers plugin.

This plugin allow you to easily have typings for your resolvers, with super flexible configuration that allow you to integrate it easily to your existing code, types and models.

That means that you can type-seal your code and have complete type-safety: your GraphQL resolvers will be typed (parent type, args, inputs, return value, context type), and you can use your own TypeScript type models so you can have type-safety all across your implementation, from API to database.

Having type check on your resolvers can help to improve your code quality, detect issues in build time (instead of runtime), and improve developer experience.

Getting Started

If you are already familiar with GraphQL Code Generator, you can skip this step.

Start by installing GraphQL Codegen and the relevant plugins:

yarn add @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers

Now, create codegen.yml file with the following, make sure to point to your schema location:

schema: YOUR_SCHEMA_LOCATION_HERE
generates:
  ./resolvers-types.ts:
    plugins:
      - typescript
      - typescript-resolvers

GraphQL Code Generator uses graphql-tools so you can point to your schema files, or /graphql endpoint.

To run GraphQL Codegen, use: yarn graphql-codegen (or, create a script for it if you wish). This should create a new file called resolvers-types.ts in your codebase.

Simple Resolvers Signature

In this example, we'll use the following GraphQL schema and resolvers as reference. You can find a working live-demo of this part here.

This is a naive implementation of a GraphQL API, and we'll see how more advanced use cases could be implemented in the upcoming steps.

To get started with the generated files, import Resolvers identifier from your generated file, and use it to type your resolvers object, for example:

Now, TypeScript engine will verify that object you returned, and you'll be able to see that if you'll change one of the fields, it will be type checked immediately:

Also, if you'll change your schema types and re-run the codegen (or use Watch Mode), it will re-generate the matching types and check your code again.

As your probably understood, the default behavior of typescript-resolvers is using the base type generated by typescript, that means, that your schema types and resolvers needs to match and have the same signature and structure.

But it's not always the case - because your GraphQL schema, in most cases, isn't the same as your models types - this is why we have mappers configuration.

Use Your Model Types

Models types are the way your data is being stored or represented behind the scenes. Think about User object from example - in most cases, the representation of User in your database (or any other downstream API) is different than the way your represent User in your API. Sometimes it's because of security considerations, and sometimes because fields are internal and used only by you, and not by the consumers.

Those model types are the actual objects that you are usually using in your resolvers code. Those can be created manually (with a simple TypeScript type, interface or class), or created automatically from your downstream APIs, database or any other data-source that you use in your app.

The way to tell codegen where are your models types are located is called mappers.

To use mappers configuration, we need first to setup a real type safety, and have models types for our actual objects.

Let's assumes that your backend is implemented this way, with some models types:

It means that now, your resolvers implementation needs to adjusted and handle the different data structure:

Noticed the errors? it caused by the fact that we don't have the appropriate mapping set yet. We need to tell GraphQL codegen that our schema types are different than the model types.

To do that, let's update codegen config with mappers and add a mapping between a GraphQL type to a TypeScript type (and the file it's located in):

This way, GraphQL Codegen will use your custom models types in the generated output, instead of the default types, and your resolvers implementation will look like that:

Note that now you'll get autocomplete, type safety and a better connection between your GraphQL schema and your GraphQL resolvers:

Typed Context

typescript-resolvers also supports replacing the context type of your resolvers implementation. All you have to do, is to add this following to your codegen configuration:

schema: schema.graphql
generates:
  ./resolvers-types.ts:
    config:
      contextType: models#MyContextType
      mappers:
        User: ./models#UserModel
        Profile: ./models#UserProfile
    plugins:
      - typescript
      - typescript-resolvers

This will make sure to replace any with MyContextType, and you'll be able to access a fully-typed context object in your resolvers.

What's next?

A few notes that worth mentioning:

  • You can use mappers on every GraphQL type, interface or a union.
  • Your resolvers' arguments (args) are also fully-typed, according to your schema definition.
  • The parent value is also fully typed, based on your mappers.
  • You can import your types from a node module package (User: models-lib#UserType).
  • You can also map to built-in language types (DateType: Date)
  • Aliasing the imports is also possible (User: ./models#User as MyCustomUserType)

You can also modify the default mapper (defaultMapper) and allow partial resolution, this will allow you to return partial objects in every resolver (more info):

config:
  useIndexSignature: true
  defaultMapper: Partial<{T}>

For more advanced use-cases, you can find the complete plugin documentation here.

15