Best Practices for integrating GraphQL Code Generator in your frontend applications

Dotan Simha
Best Practices for integrating GraphQL Code Generator in your frontend applications - The Guild Blog
Looking for experts? We offer consulting and trainings.
Explore our services and get in touch.

In this article we'll try to explain and demonstrate common patterns for frontend development with GraphQL and GraphQL Code Generator.

Most patterns are general and can be applied to most popular frontend frameworks (React, Angular, Vue, Stencil), and to popular GraphQL client libraries (Apollo / Urql), due to extended support of GraphQL Code Generator and it's flexibility.

In this article, we'll cover the development workflow of frontend applications with TypeScript and GraphQL Code Generator, suggest best-practices for GraphQL development for frontend developers, and try to explain the idea behind it.

Why do I need GraphQL Code Generator in my project?

Let's start by understanding the need for GraphQL Code Generator in your project.

If you are using TypeScript for frontend development, you probably aim to get the most out of the TypeScript type-system, that means, that your preference should be to have typed variables all across your application.

It starts with the code you write - UI components, services and business logic. You can also have type safety for your third-party libraries (some built-in, and some with @types/... packages).

The idea behind type-safety is to make sure that your code can be statically analyzed and built, before running it. It's useful because this way your can detect potential bugs before they happen in runtime.

But what about the data your fetch from external services?

So if you are already using GraphQL, you probably know that your GraphQL API is typed, and built as a GraphQL schema.

And it doesn't matter which language or platform is used to write your GraphQL API or schema - you fetch it the same way into your frontend application - with GraphQL operations (query / mutation / subscriptions, and fragment) and probably over HTTP.

So if your GraphQL schema is typed already, and your GraphQL operations allow you to choose specific fields from it (called Selection Set), why not leverage the schema and selection set and turn it into TypeScript types?

Basic data fetching with GraphQL

Let's assume that we have the following simple GraphQL schema:

scalar Date

type Query {
  upcomingEvents: [Event!]!
}

type Event {
  id: ID!
  title: String!
  date: Date!
  location: Location!
}

type Location {
  name: String!
  address: String!
}

And the client-side application consumes it with the following query:

query listEvents {
  upcomingEvents {
    id
    title
    date
  }
}

If you client-side application only needs id, title and date from the Event type - you can expect to have those fields in your GraphQL response.

You can also use it in your component code:

export const ListEvents = (listEvents) => {
  return (
    <ul className="list-events">
      {listEvents.map((event) => (
        <li key={event.id}>
          {event.title} ({event.date})
        </li>
      ))}
    </ul>
  );
};

In the example above we have a few issues that might be bugs in the future:

  1. We don't know the type of listEvents - and we can't really know it without creating a type for it manually (but this could also break, because the API could change).
  2. We can't be sure what are the actual types of id, title and date fields - it's any.
  3. We can't count of the fields to be there because they GraphQL query can change, and it's not connected to our code at all.
  4. If you'll try to access location of the event - you'll just get undefined because it's not part of the selection set.

With GraphQL Code Generator, you can have a full type safety, based on your GraphQL schema and your GraphQL operations, and that means:

  1. You can tell what is the exact structure of listEvents, what could be null and enjoy auto-complete in your IDE.
  2. You can tell what is the data type of all fields.
  3. If your selection set changes, it being reflected automatically and you can detect issues while developing or building (instead while running).
  4. Trying to access fields that are not defined in your selection set will show an error in build time and in your IDE.

So those are the basic types that codegen can generate for your, and you can get those by using the @graphql-codegen/typescript and @graphql-codegen/typescript-operations plugins of GraphQL Code Generator.

But that's not all - you can generate much more - you can get React Hooks, Angular Services and more.

How do I start?

You can start by trying GraphQL Code Generator plugin in the live-demo here and with the Getting started with GraphQL Code Generator.

Tips & Best Practices when using GraphQL Code Generator and TypeScript

Now that you understand why and how GraphQL Code Generator can help you, it's time to learn new concepts that might simplify the way your consume GraphQL API, and improve your code quality.

Watch Mode

GraphQL Code Generator also comes with a built in watch mode. You can use it from the CLI:

graphql-codegen --watch

Or, set it in your codegen.yml file:

watch: true
schema: ...

This way, each time you have changes for your GraphQL schema or GraphQL operations, GraphQL Code Generator will be executed again and update the generated files.

Generate more than just types

GraphQL Code Generator can generate more than just TypeScript types. It can automate some of your GraphQL development workflow, generate common practices for data fetching, and add type-safety to code you usually need to write manually.

Beside TypeScript types, here's a list and examples of part of GraphQL Codegen capabilities:

Dump remote schema to a local file

If your GraphQL schema is only available for you using an HTTP endpoint, you can always get a copy of it locally. This is useful for better IDE experience.

You can do it with the @graphql-codegen/schema-ast plugin, and the following configuration:

schema: http://YOUR_SERVER/graphql
generates:
  ./src/schema.graphql:
    plugins:
      - schema-ast
Save local GraphQL Introspection

GraphQL schema can be represented in many ways. One of the is introspection.

You can save a local copy of your schema introspection using @graphql-codegen/introspection and the following:

schema: YOUR_SCHEMA_PATH
generates:
  ./src/schema.json:
    plugins:
      - introspection
Add custom content to output files

I you wish to add custom content to the codegen output files, you can use @graphql-codegen/add plugin, and add your content this way:

schema: YOUR_SCHEMA_PATH
generates:
  ./src/types.ts:
    plugins:
      - add: '// THIS FILE IS GENERATED, DO NOT EDIT!'
      - typescript
React & Apollo: Generate Hooks

You can generate ready-to-use React hooks for your GraphQL operations, with the following configuration:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.tsx:
    config:
      withHooks: true
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

And then use it in your code:

import React from 'react';
import { useMyQuery } from './generated-types';

export const MyComponent: React.FC = () => {
  const { data, loading, error } = useMyQuery();

  // `data` is now fully typed based on your GraphQL query

  return <> ... </>;
};
React & Apollo: Generate HOC (High-Order-Component)

You can generate ready-to-use React HOC for your GraphQL operations, with the following configuration:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.tsx:
    config:
      withHOC: true
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

And then use it in your code:

import React from 'react';
import { withMyQuery } from './generated-types';

const MyViewComponent: React.FC = ({ data, loading, error }) => {
  // `data` is now fully typed based on your GraphQL query

  return (<> ... </>);
};

export const MyComponent = withMyQuery({
  variables: { ... }
})(MyViewComponent);
React & Apollo: Generate Components

You can generate ready-to-use React data components for your GraphQL operations, with the following configuration:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.tsx:
    config:
      withComponent: true
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

And then use it in your code:

import React from 'react';
import { MyQueryComponent } from './generated-types';

export const MyComponent: React.FC = ({ data, loading, error }) => {

  return (
    <MyQueryComponent variables={...}>
      {
        ({ data, loading, error }) => {
          // `data` is now fully typed based on your GraphQL query

          return (<> ... </>)
        }
      }
    </MyQueryComponent>
  );
};
Angular & Apollo: Generate Services

You can generate ready-to-use Angular Services for your GraphQL operations, with the following configuration:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.ts:
    config:
      withHooks: true
    plugins:
      - typescript
      - typescript-operations
      - typescript-apollo-angular

And then use it in your code:

import { MyFeedGQL, MyFeedQuery } from './generated-types';

@Component({
  selector: 'feed',
  template: `
    <h1>Feed:</h1>
    <ul>
      <li *ngFor="let item of feed | async">{{ item.id }}</li>
    </ul>
  `,
})
export class FeedComponent {
  feed: Observable<MyFeedQuery['feed']>;

  constructor(feedGQL: MyFeedGQL) {
    this.feed = feedGQL
      .watch()
      .valueChanges.pipe(map((result) => result.data.feed));
  }
}
React & Urql: Generate Hooks

If you are using urql as your GraphQL client, you can generate ready-to-use React hooks for your GraphQL operations, with the following configuration:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.tsx:
    config:
      withHooks: true
    plugins:
      - typescript
      - typescript-operations
      - typescript-urql

And then use it in your code:

import React from 'react';
import { useMyQuery } from './generated-types';

export const MyComponent: React.FC = () => {
  const { data, loading, error } = useMyQuery();

  // `data` is now fully typed based on your GraphQL query

  return <> ... </>;
};
tip

This plugin can also generate HOC or data Component, based on your preference ;)

Vue.js & Apollo: Generate Composition Functions

If you are using Vue.js with @vue/apollo-composable your GraphQL client, you can generate composition functions based on your GraphQL operations:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.ts:
    config:
      withHooks: true
    plugins:
      - typescript
      - typescript-operations
      - typescript-vue-apollo

And then use it in your code:

<template>
  <div>
    {{ result.feed.id }}
  </div>
</template>

<script lang="ts">
import { createComponent } from "@vue/composition-api";
import {
  useTestQuery,
} from "../generated-types";

export default createComponent({
  setup() {
    const { result } = useMessagesQuery();

    return { result };
  }
});
</script>
Apollo: type-safe `refetchQueries`

If you are using Apollo Client, and you wish to refetch a query when a mutation is done, you can do add @graphql-codegen/named-operations-object plugin to your setup.

It will generate a const object that contains a list of your GraphQL operation names, as found by the codegen. This is useful because if you'll change the name of your operation, you'll know about it in build time, and you'll be able to update it:

This is how to configure it:

schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
  src/generated-types.ts:
    plugins:
      - typescript
      - typescript-operations
      - named-operations-object

And then use it in your code:

import { client } from './apollo'; // this is your Apollo Client instance, for example
import { addTodoMutation, namedOperations } from './generated-types';

client.mutate({
  query: addTodoMutation,
  variables: { ... },
  refetchQueries: [
    // If you'll change or remove that operation, this will fail during build time!
    namedOperations.Query.listTodo,
  ]
})
note

You can use it with any other wrapper of Apollo-Client, such as apollo-angular or react-apollo.

Apollo: auto-generated `fragmentMatcher` / `possibleTypes`

If you are using Apollo-Client and your schema contains GraphQL union or interface, you'll need to provide fragmentMatcher to your Apollo store instance.

This is needed in order to improve performance of Apollo store. You can read more about this here.

You can generate it using the following configuration:

schema: YOUR_SCHEMA_PATH
generates:
  ./src/fragment-matcher.ts:
    plugins:
      - fragment-matcher

And then pass it directly to your Apollo instance:

import { InMemoryCache } from '@apollo/client';

// generated by Fragment Matcher plugin
import introspectionResult from '../fragment-matcher';

const cache = new InMemoryCache({
  possibleTypes: introspectionResult.possibleTypes,
});

Name your operations

It's highly important to name your GraphQL operations, because otherwise it will be difficult for your GraphQL client to cache and manage it. It will also make it difficult for the codegen to create easy-to-use types, and it will fallback to Unnamed_Operation_.

✅ Do:

query myOperationNameHere {
  ...
}

❌ Don't:

query {
  ...
}
Duplicate Names

Ensure you have unique names for your operations.

Libraries like Apollo Client will have issues and unexpected behavior if you re-use the same operation name, and GraphQL Code Generator will throw an error in case of name duplications.

Write your operations and fragments in .graphql files

You can manage your GraphQL operations in .graphql files, without worrying about loading it into your application with Webpack loaders or anything else. Also, Most IDEs has better support for autocomplete inside .graphql files.

GraphQL Code Generator plugins for frontend frameworks integrations (such as typescript-react-apollo / typescript-apollo-angular) are automatically creates an executable copy (DocumentNode) of your GraphQL operations in the generated code file, and it will automatically include it withing your wrapper call.

It will add that to the output file with Document suffix, and FragmentDoc for fragments.

So you can maintain your operations in .graphql files, but import it from the generate code file:

// MyQueryDocument and MyUserFragmentDoc are parsed `DocumentNode`
import { MyQueryDocument, MyUserFragmentDoc } from './generated-types';
No need to handle imports

If you have a query that uses a fragment, you can just use the fragment spread as-is, without the need to import it or maintain it in the same file.

For example:

# user.query.graphql
query user {
  userById {
    ...UserFields # We don't need to import this, just use the name
  }
}
# userfields.fragment.graphql
fragment UserFields on User {
  id
  name
}

And if you'll import UserQueryDocument from your generated file, it will have the fragment concatenated automatically.

Fragment per component

If you wish to have a simple way to manage your application complexity with multiple queries and fragments, consider to have small fragments that defines the needs of your components.

Consider the following structure for example (for a list and item implementation):

src/
├── generated-types.tsx
├── list/
├──── todo-list.tsx
├──── todo-list.query.graphql
├── list-item/
├──── todo-item.tsx
├──── todo-item.fragment.graphql
├── todo-details/
├──── todo-details.tsx
├──── todo-details.fragment.graphql
├── user-profile/
├──── profile-page.tsx
├──── me.query.graphql
├──── authenticated-user.fragment.graphql

Then, your GraphQL query files can just build it's self based on the nested fragments it needs:

# todo-list.query.graphql
query todoList {
  todos {
    ...TodoItemFields
    ...TodoDetailsFields
  }
}
# me.query.graphql
query me {
  me {
    ...AuthenticatedUserFields
  }
}

And then, GraphQL Code Generator will generate a matching TypeScript type per each component, based on the fragment or query that it needs.

So you can use the generated fragment type as the input for your components, and pass it directly from the parent component easily, with type-safety:

// todo-list.tsx
import React from 'react';
import { useTodoList } from '../generated-types';
import { TodoItem } from './todo-item';

export const TodoList: React.FC = () => {
  const { data, loading, error } = useTodoList();

  return (
    <>
      {data.todos.map((todo) => (
        <TodoItem todo={todo} />
      ))}
    </>
  );
};
// todo-item.tsx
import React from 'react';
import { TodoItemFieldsFragment } from '../generated-types';

export const TodoItem: React.FC = (todo: TodoItemFieldsFragment) => {
  return <div>{todo.title}</div>;
};
note

Please have some judgment before creating fragments, it should represent data structure that is specific per component. Don't abuse this mechanism by creating fragments with a single field. Try to group it in a way that matches your components needs.

Access to nested generated types

If you are already familiar with plugins such as @graphql-codegen/typescript-operations output structure, you probably already know that it's built on operations and fragments.

It means that each GraphQL query and each GraphQL fragment that you have, will be converted into a single TypeScript type.

That means, that accessing nested fields in your generated TypeScript types might looks a bit complex at the beginning.

Consider the following query:

query userById($userId: ID!) {
  user(id: $userId) {
    id
    profile {
      age
      name {
        first
        last
      }
    }
  }
}

The @graphql-codegen/typescript-operations plugin output for that query will be:

export type UserByIdQuery = { __typename?: 'Query' } & {
  user?: Maybe<
    { __typename?: 'User' } & Pick<User, 'id'> & {
        profile?: Maybe<
          { __typename?: 'Profile' } & Pick<Profile, 'age'> & {
              name: { __typename?: 'Name' } & Pick<Name, 'first' | 'last'>;
            }
        >;
      }
  >;
};

Accessing the actual TypeScript type of user.profile.name.first might look a bit intimidating, but there are several things you can do to simplify the access to it:

  • Best solution: use fragments - if you'll use fragments for the User fields and for Profile fields, you'll break down the types into smaller pieces (see previous tip).
  • Use TypeScript type system: type FirstName = UserByIdQuery['user']['profile']['name']['first'].
  • You can also do it with Pick: type FirstName = Pick<UserByIdQuery, ['user', 'profile', 'name', 'first']>.
Hate Pick in your generated files?

The @graphql-codegen/typescript-operations is the TypeScript representation of your GraphQL selection set. Just like selection set chooses fields from the GraphQL schema, typescript-operations picks fields from typescript plugin (which is the representation of your GraphQL schema).

If you wish to have simpler TypeScript output, you can set preResolveTypes: true in your configuration, and it will prefer to use the primitive TypeScript type when possible.