Introduction/Summary

GraphQL has become a popular choice for building APIs in recent years. In projects using Typescript and Apollo Client, such as Rubrik’s, it is very helpful to map GraphQL schema to types and interfaces and one of the most popular tools for generating these types and interfaces based on a GraphQL schema is Apollo Codegen. However, the open-source community has recently deprecated Apollo Codegen and has recommended GraphQL Code Generator instead, which aims to provide a more flexible and customizable code generation experience.

At Rubrik, we have recently migrated from Apollo codegen to GraphQL Code Generator and if you're interested in doing the same, this guide will help you through the process. The migration was also inspired by the github issue created by one of the authors of GraphQL Code Generator.

What is GraphQL Code Generator and how is it different from Apollo Codegen?

GraphQL Code Generator is a tool that helps developers automatically generate types for their GraphQL schema and operations. By using plugins, developers can customize the code generator to suit their specific needs.

The major distinction between GraphQL Code Generator and Apollo Codegen, lies in their respective motivations. While Apollo Codegen was primarily designed to generate types specifically for the Apollo client, GraphQL Code Generator supports most of the GraphQL clients like Apollo Client, URQL and React Query.

However, some key differences between the two are as follows:

  1. GraphQL Code Generator doesn’t support generating types of nested selection sets.

 For eg: if we have the following schema:

schema {
  query: Query
}

type Query {
  search(term: String!): [SearchResult!]!
}

interface Node {
  id: ID!
}

union SearchResult = User | Chat 

type User implements Node {
  id: ID!
  username: String!
}

type Chat implements Node {
  id: ID!
  users: [User!]!
  message: String!
}


And we have the following query:

query SearchTerm($term: String!) {
  search(term: $term) {
    ... on User {
      username  
    }
    ... on Chat {
      message
    }
  }
}

Apollo Codegen would have generated a type: SearchTerm_search_User but GraphQL Code Generator won’t do that and will just generate a union of all possible types on search instead.

export type SearchTermQuery = {
  readonly search: ReadonlyArray<
    | {readonly __typename: 'User'; readonly username: string}
    | {readonly __typename: 'Chat'; readonly message: string}
  >;
};



        2. With GraphQL Code Generator’s configuration file, we can easily customize its output to match our needs.

        3. GraphQL Code Generator has 100+ plugins for various languages, platforms and frameworks.

        4. GraphQL Code Generator can also generate typed document nodes which can be easily used with GraphQL client hooks. It means that just by passing the GraphQL                query/mutation/fragment to a supporting GraphQL client library, we’ll get a fully typed result object and variables object.

        5. GraphQL Code Generator can also generate the fragments-matcher or possibleTypes object required by Apollo Client.

        6. GraphQL Code Generator also uses lifecycle hooks where you can run specific scripts at different stages of the generation.

        7. In  GraphQL Code Generator, By default, generated query types will have the suffix "Query" and generated mutation types will have the suffix "Mutation".

Prerequisites for the migration

Despite customizing the configuration file of GraphQL Code Generator to match the output of Apollo Codegen, there were still some limitations that required changes to our codebase to enable a successful migration. One such limitation was GraphQL Code Generator's inability to generate types for nested selection sets. Therefore, we identified two patterns and their respective solutions to migrate and ensure compatibility with GraphQL Code Generator's output.

  1. In case of inline fragments for interface implementations in our GraphQL operations, we have two options to handle them:

a. Convert the inline fragment to a named fragment - This approach, as shown in the User and Chat example, involves defining a named fragment that includes the fields of the interface implementation, and then referencing the fragment in the inline fragment. This approach can make your queries more readable and maintainable, especially if you need to reuse the same fragment in multiple places.

query SearchTermQuery($term: String!) {
  search(term: $term) {
    ... UserField
    ... ChatField
  }
}
fragment UserField on User {
    username
}
fragment ChatField on Chat {
    message
}

           The generated output will look like:

export type SearchTermQuery = {
  readonly search: ReadonlyArray<
    | {readonly __typename: 'User'; readonly username: string}
    | {readonly __typename: 'Chat'; readonly message: string}
  >;
};

export type UserFieldFragment = {
  readonly __typename: 'User';
  readonly username: string;
};

export type ChatFieldFragment = {
  readonly __typename: 'Chat';
  readonly message: string;
};

          Now, you can use UserFieldFragment wherever you were using SearchTerm_search_User.

b. Use TypeScript Extract - This approach involves using TypeScript's Extract utility type to extract the required interface implementation from the union type.

type UserField = Extract<SearchTermQuery['search'], {__typename: 'User'}>

In our codebase, we have followed a hybrid approach for handling inline fragments for interface implementations in GraphQL operations. Specifically, we converted all inline fragments that were used in multiple places and requested the same fields to reusable named fragments, and the ones that were used only in a single place were converted to use TypeScript Extract.

2. For referring to normal nested fields, we have to use typescript aliases, for eg: SearchTermQuery_search will become SearchTermQuery['search']. Since the number of instances were huge and the manual effort would have been a lot to migrate all such instances to typescript aliases, we wrote an AST(Abstract Syntax Tree) based codemod for this task.

Installation

First install the following packages to run GraphQL Code Generator.

pnpm add -D typescript @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/add

@graphql-codegen/typescript is a plugin for generating the base TypeScript types and refers to the exact structure of our schema.

@graphql-codegen/typescript-operations is another plugin that generates types for your GraphQL documents: Query, Mutation and Fragment.

@graphql-codegen/add is a plugin to add a comment at the top of our generated file to disable eslint.

Configuration File

The GraphQL Code Generator tool utilizes a configuration file called codegen.ts to govern various options, input formats, and output document types. So, the next step is to write a configuration file to match the output to Apollo Codegen as much as possible.

The configuration file we used is as follows:

import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  /**
   * The schema field in your code should indicate the location of your GraphQLSchema.
   * You can define schema either as a string, which refers to the location of your schema,
   * or as a string array (string[]), which points to multiple schemas that will be merged.
   * The GraphQL Code Generator documentation lists down the available formats for defining 
   * the schema field in your configuration file. You can find the list on the following page: 
   * https://the-guild.dev/graphql/codegen/docs/config-reference/schema-field#available-formats
   */
  schema: 'schema.json',
  /**
   * The documents field in your configuration file specifies an array of file paths
   * or glob patterns that export GraphQL documents using a gql tag or a plain string.
   * Alternatively, if you're dealing with a single document, you can provide these
   * options as a string instead of an array.
   */
  documents: ['src/**/*.tsx'],
  /**
   * A map where the key represents an output path for the generated code,
   * and the value represents a set of relevant options for that specific file.
   */
  generates: {
    'src/gql.types.ts': {
      plugins: [
        {
          /**
           * Since, GraphQL code generator doesn't disable eslint for us automatically,
           * we need to pass the disable comments to the content of the add plugin which will put
           * these comments at the top of our generated types file.
           */
          add: {
            content: `
/** THIS FILE IS AUTO-GENERATED **/
/** DO NOT EDIT **/
/* eslint-disable */
`,
          },
        },
        'typescript',
        'typescript-operations',
      ],
      config: {
        /**
         * We do not want to change the naming convention of our types and want it to follow the same convention
         * as that of our schema. So we pass `keep` as the value to this field. If we do not pass this,
         * there are some unwanted naming conventions added to our types such as:
         * converting all our enum values to UPPER_SNAKE_CASE which can lead to conflicts.
         */
        namingConvention: 'keep',
        /**
         * We do not want to make our query or mutation fields optional(?).
         * Hence, we pass the value of `field` of `avoidOptionals` as true. If we don't do this,
         * any null value field will become optional.
         */
        avoidOptionals: {
          field: true,
        },
        /**
         * By default, `__typename` is an optional field. But we want to override it
         * to make it a required field in all our selection sets. Hence we pass it as true.
         */
        nonOptionalTypename: true,
        /**
         * By default, the Queries and Mutations types also have __typename fields like:
         * ```
         * export interface SearchTermQuery {
         *  __typename: 'Query';
         * readonly search: ...;
         * }
         * ```
         * So, we pass true as a value to this field to avoid `__typename` in the root fields.
         */
        skipTypeNameForRoot: true,
        /**
         * Makes all the query and mutation fields `readonly`.
         * This is equivalent to the `useReadOnlyTypes`
         * flag of Apollo Codegen
         */
        immutableTypes: true,
        /**
         * Apollo Codegen used to generate interfaces by default.
         * Hence we pass `interface` as the value to this field.
         */
        declarationKind: 'interface',
        /**
         * If we omit this field, all query names will have "Query" as a suffix.
         * However, since we already follow the convention of adding "Query" as
         * a suffix to our query names, this may not be necessary.
         * But, even after passing this, if some query or mutation names
         * are not suffixed with "Query" or "Mutation", the codegen will
         * automatically add the appropriate suffix.
         */
        dedupeOperationSuffix: true,
        /**
         * This field prevents the generation of all fields in our schema.json.
         * Only the fields used in our GraphQL documents are generated.
         */
        onlyOperationTypes: true,
        /**
         * By GraphQL convention any query fields which are arrays have type
         * `ReadonlyArray<T> | <T>` which is the default type generated by GraphQL code generator.
         * But since Apollo Codegen used to generate them only as arrays i.e `ReadonlyArray<T>`,
         * we pass this field as true to avoid any typescript errors.
         */
        arrayInputCoercion: false,
        /**
         * We pass all the custom scalars and their types that we want to define other
         * than common GraphQL types to this field.
         */
        scalars: {
          Long: 'number',
        },
      },
    }
  }
}
 
export default config

Running the codegen script

As the last step, you can now add a script to the package.json to run the GraphQL Code Generator.

{
  "scripts": {
    "generate": "graphql-codegen",
  }
}

Now, you can run the script pnpm generate -config codegen.ts to generate the output.

Alternatively, you can also make use of the generate function provided by GraphQL Code Generator to generate the outputs programmatically

import {generate} from '@graphql-codegen/cli';
import config from 'codegen.ts'

export function generateOutput() {
  return generate(
    config,
    // this parameter is for saving the file to gql.types.ts, skipping this param will only generate the results but not save them
    true,
  );
}

Conclusion

In summary, although Rubrik's migration from Apollo Codegen to GraphQL Code Generator was not particularly difficult, we understand that others may face challenges during the process. We hope that this article will serve as a useful resource for those who may need guidance and support in transitioning to GraphQL Code Generator for their TypeScript and GraphQL projects.