Generated GraphQL: Part 3

I've been on a quest to make it easier to use GraphQL in a reliable and high-performance way using Node and TypeScript. My quest has brought me to the lofty peaks of PostGraphile and GraphQL Code Generator, and today I'm ready to wire in my Create React App-based front-end!

Part 1 covered the goals of the architecture, and introduced how to use PostGraphile with Koa. Part 2 walked through how to extend the automatic schema and build business logic, and introduced the generator for TypeScript types. Today's Part 3 will introduce a React front end and show how to use those types to connect your Components to your API.

Establishing a Connection

The first thing we'll need is an API Client module to connect to your shiny new API. I enjoy the Apollo Client React library, especially with the GraphQL Code Generator types. It's not the only choice, however - you could also consider the hilariously named urql. In my case, I install Apollo Boost and get to work!

yarn add apollo-boost apollo-cache-inmemory react-apollo graphql graphql-tag

I create a file that I call src/data/ApiClient.ts, but you can call it whatever you'd like.

import 'isomorphic-fetch'
import ApolloClient from 'apollo-boost'
import {NormalizedCacheObject} from 'apollo-cache-inmemory'

import Config from '../Config'
import {LocalStorage} from './BrowserStorage'

type FromPromise<T> = T extends Promise<infer U> ? U : T
export type ApiClient = FromPromise<ReturnType<typeof create>>

export const create = async () => {
  const uri = `${Config.baseUrl}/graphql`

  return new ApolloClient<NormalizedCacheObject>({
    uri,
    request: async (operation) => {
      const user = LocalStorage.getUser()

      operation.setContext({
        headers: {
          authorization: user ? `Bearer ${user.idToken}` : `Bearer anon`,
        }
      })
    }
  })
}

export default {create}

Config is a module that pulls in various settings based on environment variables. Config.baseUrl in this case works out to http://localhost:4000, which is where I have my PostGraphile application running. LocalStorage is our wrapper around the browser API.

The first thing I do is export a handy type that I can use in various places where I want to require an ApolloClient instance.

Then, I define a quick create() function that returns a Promise for a client instance. This is used during my React application's bootstrap process and passed to ApolloProvider on the outside of my JSX tree:

const client = await ApiClient.create()

ReactDOM.render(
  <ApolloProvider client={client}>
    {/* ... */}
  </ApolloProvider>,
  document.getElementById('root')
)

Generating Types

I keep the results of my GraphQL Code Generator output in a file called src/data/Schema.tsx. To configure the generator, I add a codegen.yml file to the root of my project:

schema: 'http://localhost:4000/graphql'
documents: ./src/**/*.graphql
generates:
  src/data/Schema.tsx:
    plugins:
      - add: '// WARNING: This file is automatically generated. Do not edit.'
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      namingConvention: change-case#pascalCase
      withHOC: false

I use the withHOC: false setting because we work entirely with Apollo's render-prop based Components.

Next, I create a new "scripts" entry in my package.json file:

  "generate.schema": "graphql-codegen",

Now, we're ready to make some Queries!

Moving Some Data

PostGraphile exposes a core Schema, and now we need our front end to define usages of that Schema in the form of Documents. GraphQL Code Generator automatically generates TypeScript types based on the Documents we define, and even produces some components that automatically wire in those types for convenience.

Core Schema

The core Schema is generated by the PostgreSQL table and column structure, and altered by a number of PostGraphile plugins that the API uses to extend the Schema. This Schema makes up all of the available GraphQL Operations (Queries, Mutations, Subscriptions) that are supported by the API, and the input and output types that these Operations work with.

GraphQL Code Generator generates some basic types that cover the entire core Schema, but in most cases you don't want to use these types. For example, the User type that is automatically generated based on the example API in Part 2 includes all fields that could possibly be available on the type, including nodeId - a value used for cache identity. This is usually not what you want.

Rather than using these core types directly, in most cases you'll want to define a usage of the Schema through a GraphQL Document, and define Fragments if you need reusable types that you can easily share between Components.

Documents

GraphQL Documents are defined in individual *.graphql files. I usually store them within the src/data folder in my projects, but you can keep them anywhere that GraphQL Code Generator can find them. These represent client-side usages of the Schema, and they are what you will most often use inside the front end.

Each .graphql file will contain multiple Documents, and they will also optionally contain multiple data Fragments as mentioned above. Here's an example of a Query, defined within a file called src/data/users.graphql:

query userByIdForDashboard($id: Int!) {
  userById(id: $id) {
    id
    displayName
  }
}

This Query defines a specific set of fields that are needed when retrieving a single User for a hypothetical Dashboard. GraphQL Code Generator will read the users.graphql file that this Document is defined within, and it will generate some types:

  • UserByIdForDashboardDocument - the GraphQL Query definition used by Apollo when executing the operation.
  • UserByIdForDashboardQuery - the data property in the GraphQL response. There will be a top-level key in the object for each operation executed by the Document. In this case, there would be a single property with a key of userById, because that's the only operation we executed in this Document.
  • UserByIdForDashboardQueryVariables - the variables needed in order to execute the Document.

It also generates a Component:

  • UserByIdForDashboardComponent - an Apollo Query component with the proper types already applied.

When using this Component, you would pass a render function that expects QueryResult<UserByIdForDashboardQuery>. QueryResult comes from react-apollo. This provides the result.loading, result.error, and result.data properties that we work with when we get a response back from the API.

render () {
  return (
    <UserByIdForDashboardComponent
      variables={{id: userId}}>
      {this.renderDashboard}
    </UserByIdForDashboardComponent>
  )
}

renderDashboard = (result: QueryResult<UserByIdForDashboardQuery>) => {
  // ...
}

Fragments

Often, you will find that you have a set of properties on a type that you want to reuse in multiple places in your code. For example, let's look at a similar Query that selects a different set of fields:

fragment UserAccountData on User {
  id
  name
  accountByAccountId {
    id
    email
    attributes
    addressByMailingAddressId {
      city
      state
    }
  }
}

query userByIdWithAccountData($id: Int!) {
  userById(id: $id) {
    ...UserAccountData
  }
}

This defines a Fragment, UserAccountData, which selects more fields from the User type. It works essentially the same as the userByIdForDashboard Query above, but it generates an additional type that you can import from the Code Generator results:

  • UserAccountDataFragment - For each Fragment defined in your *.graphql files, a type is generated in the Schema with a suffix of Fragment added to the name.

This gives us a convenient type to import and use accross the application.

If you want to use Pick<> to select individual fields on a type, prefer a Fragment over the full core Schema type that is generated. The full core Schema types define Connections which reference other full core Schema types, which include things like nodeId and will often cause type problems. Fragments link to Connections which select only the fields that were requested, so they are usually the better choice.

Tips

Getting Nodes or Edges

export interface Connection<T> {
  nodes?: T[]
  edges?: {
    cursor?: string
    node?: T
  }[]
}

export const stripEmptyElements = <T>(array: Array<T>) =>
  array.filter(Boolean) as NonNullable<T>[]

export const getNodesFromConnection = <T>(pagination?: Connection<T> | null): NonNullable<T>[] => {
  if (!pagination) {
    return []
  }

  if (pagination.nodes) {
    return stripEmptyElements(pagination.nodes)
  }

  return pagination.edges
    ? stripEmptyElements(pagination.edges.map(edge => edge.node))
    : []
}

To retrieve Nodes or Edges from my Connections, I use this utility to make it easy to pull the underlying TypeScript type out and handle any nullable array elements.

The handling of nullable array elements is unnecessary if you're using the --no-setof-functions-contain-nulls configuration flag with PostGraphile, but I handle them in the utility just in case I need to in a project that cannot use that setting.

nodeId Errors

This usually means that a component that you're using or some other custom interface is pulling in a core type from Schema.tsx, rather than from a Fragment or a Query response.

In most cases, you'll want to trace your data back to the source and use the most specific Fragment you can. For abstract components that might work with data from multiple sources, use the simplest Fragment you can and select the specific fields you need from it using Pick<>.

Fragments with Arguments

Sometimes you might have a Connection that takes arguments:

fragment UserWithScore on User {
  id
  name
  score(gameId: $gameId) {
    id
    points
    duration
  }
}

query allUsersWithScore(
  $gameId: Int!
  $first: Int
  $last: Int
  $offset: Int
  $before: Cursor
  $after: Cursor
  $orderBy: [UsersOrderBy!] = [PRIMARY_KEY_ASC]
  $condition: UserCondition
) {
  allUsers(
    first: $first
    last: $last
    offset: $offset
    before: $before
    after: $after
    orderBy: $orderBy
    condition: $condition
  ) {
    nodes {
      ...UserWithScore
    }
  }
}

In this case, the UserWithScore Fragment uses the score() Connection on the User type. This Connection takes an argument of gameId: Int!. Assume for a moment that the you have a score table with a game_id column and a user_id column. If you wanted to select a set of Users and see what their score on a particular Game was, this would be the Query for you!

As you can see, there's a host of built-in arguments that we define and pass-through to the allUsers Query here - a built in PostGraphile operation. We define an additional argument that allUsers doesn't take, however - $gameId: Int!. This $gameId variable is passed on to the UserWithScore Fragment, which makes use of it with the score() Connection.

There's one gotcha with this approach - you can't pull out nested properties from inputs like UserCondition. If for some reason your UserCondition also accepted a gameId variable, you'd need to pass it twice:

render () {
  return (
    <AllUsersWithScoreComponent
      variables={{gameId: id, condition: {gameId: id}}}>
      {this.renderUsers}
    </AllUsersWithScoreComponent>
  )
}

In this example, the id variable is passed in the gameId position both on the outside of the condition input, and on the inside. This is redundant, but due to limitations with the GraphQL Query Language, it's necessary if you want to use property the property in both places.

Onward!

Now that we've gotten this outstanding architecture in place for our project, I'll be continuing to blog about it to share more tips, tricks, and workarounds as we discover them.

I'll also be recording screencasts in the weeks ahead, demonstrating a full backend and frontend project from start to finish. Let me know what you'd like to see, and I look forward to answering any questions you have via Twitter or email!

Thanks for reading!