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
- thedata
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 ofuserById
, because that's the only operation we executed in this Document.UserByIdForDashboardQueryVariables
- thevariables
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 ofFragment
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!