Bridging the server client gap: GraphQL, Typescript and Apollo codegen
That which is to be demonstrated:
- Dump schema from your GraphQL server
- Generate TypeScript interfaces for Queries, Fragments, Mutation variables and payloads
- Use these in React components to guarantee that all your types are what the server is supplying (or for mutations: what the server is requiring)
Programming is the art of managing complexity. One particularly ineffective technique is filling your skull with hundreds of function signatures until you can no longer communicate verbally with anyone else in the room. Other equally error prone methods are “naming conventions” and “proper documentation”.
We should be using our working memory for better things like designing applications that are usable by happy humans, building fascinating stuff and solving the important problems of our generation.
Let’s get the computers to do what they are good at: being inflexible and pedantic. Static typing eliminates errors at compile time, frees up human working memory and enables your IDE to show correct types and documentation while you type.
The server-client type safety gap
After you’ve gotten your client code fully typed, you feel a sense of security. Soon enough you might notice that there all kinds of bugs due to getting something different from the server or sending something wrong to it. This is the server-client type safety gap.
GraphQL servers create a schema which is fully and explicitly typed. Every field, query variable, mutation input variable and mutation payload is typed. Any GraphQL server that you are using will export this to a large JSON file.
I’m using django-graphene as my GraphQL server at the moment, so the field types flow from the original Django model definition into the schema.
A django.db.models.fields.BooleanField(null=False) on a model becomes:
{
"name": "isStaff",
"description": "Designates whether the user can log into this admin site.",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
apollo-codegen is a tool that can read this schema file, compile all your application’s queries, fragments and mutations and generate TypeScript interfaces. (It can also generate flow-type and Swift.)
Given a Django model:
and a GraphQL query (in this case a fragment):
we generate TypeScript using apollo-codegen:
which creates a file called queries.ts with all types in the application. It generates the following type definitions:
{Name}Query{Name}QueryVariables{Name}Fragment{Name}Mutation{Name}MutationVariables
From that file, here is our fragment:
We can now import MessageFragment into the Message component that uses it:
We are now assured (by threat of squiggly red lines and compile errors) that every parameter exists and is the expected type. Author has a name and it’s a string. DateFormat date gets a string and not a Date object.
If the backend guy fiddles with the API and dates are now serialized as unix timestamp integers and he don’t tell nobody, rest assured your faithful squiggly red lines will alert you. You will not suffer the misery of finding out after you did a release. You will not even have to wait for unit tests to run.
In fact we haven’t even used this component in the real world, but we have assured that given its fragment definition, the component uses the data correctly.
Hooking it all up
TODO: explain why the query must be required not imported. tsconfig.json might be involved.
Errors, glorious errors
These are with VSCode
Fragments
You may have noticed above that the queries and fragments are in a separate file, not a gql tag in the component source like this:
apollo-codegen can parse your .tsx or .js files and find and extract any gql tags, but one thing it cannot do is variable interpolation. So concatenating fragments that way will not work. It isn’t actually running your code or your app—that could cause all kinds of problems—so it has no way of knowing what variables are in the local scope, so it cannot interpolate.
So fragments are imported using comment syntax and can be included by using a special webpack loader: graphql-tag/loader
#import './Message.graphql'
At first this seems annoying. You have extra files, some of them very small. But you don’t have any static .fragments on your components classes, nor any string interpolation. It actually turns out to be a bit clearer and easier to maintain.
You might think it’s nice to have the graphql query on the same page as your component, but once you’ve generated a type then this is much more informative:
This is going to compile your graphql queries at build time.
PRO: You can avoid having your puny mobile phone compile all queries and mutations when your app starts up. You can include a fragment multiple times without increasing file size.
CON: The file size of the compiled queries is a bit bigger than the GraphQL query strings, but you can avoid including graphql-tag in your app. You are furthering your “webpack ecosystem” addiction.
Workflow
I have this defined in my .bash_profile
(This is a zsh function):
nsgen () {
cd ~/Sites/beep
vagrant ssh --command="cd /ns/ && DEBUG=0 python manage.py graphql_schema"
apollo-codegen generate frontend/react/**/*.graphql --schema frontend/schema.json --target typescript --output frontend/react/queries.ts
cd ~/Sites/beep/frontend
node generate-mutations-tsx.js > react/components/mutations.tsx
}
Any time I make changes to my python schema or .graphql files I run this ( nsgen
).
My Django development runs inside a Vagrant box. This runs a django command to dump the latest schema and then runs apollo-codegen to update queries.ts
The last line runs a script to generate a file called mutations.tsx. I’ll tackle that one in a further article on mutations.