How to configure GraphQL-Codegen and React Apollo Client to do work for you
I’m writing this as I am part-way through my 4th React / React Native app which uses Apollo Client, and I’ve just noticed how much time I have saved by configuring GraphQL-Codegen correctly. By the end of this article, you should hopefully know how to generate TypeScript types and enums for your GraphQL types and type-safe React hooks for your queries and mutations. This will lead to a consistent and reliable way of working with data throughout your app.
GraphQL-Codegen Setup
I am going to assume that you have a GraphQL backend already setup and running. In my latest project I’m thoroughly enjoying Hasura and Nhost, but you could choose GraphQL Yoga with Prisma, Graphcool or whatever you choose. You just need the endpoint and any auth credentials.
First you will need to install the required GraphQL-Codegen dependencies:
yarn add --dev @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
And in your package.json
, add a couple of scripts to make your life easier:
"scripts": {
"generate": "graphql-codegen --config codegen.js -r dotenv/config .env",
"generate:watch": "yarn generate --watch"
}
You will probably also want to add yarn generate &&
to your build command to ensure you build your latest model set before building your app.
The generate
function will pass in any environment variables from your .env
file, and use the config file codegen.js
, which we are just about to create. It’s important to keep your backend’s hostname and any secrets in a .env
file as you may use multiple environments and shouldn’t commit secrets to your repo. I’ve chosen to use a JS file for the config as I find it to be the most readable, and it also supports comments, but you can use a YAML file or JSON.
You will run the watch command in the background while developing. Not only does this generate stuff for you, but it also checks your GraphQL logic against your backend as you are writing it and gives really handy error messages, so you will catch errors early.
This is where the magic happens
As you can see, this shows how the environment variables are added and used in the config. Note: If you don’t recognise the syntax, I’m using the computed property names syntax to use the API_URL
env variable as on object key.
The documents
key indicates the input files to use to generate the output. We are going to write all our GraphQL in .graqhql
files within the ./api
directory.
GraphQL-Codegen is then going to generate the ./api/index.ts
file. This means when we are using the generated code, we can import it from the root api
directory.
We do not want HOCs or Components as they are soooo last year, hooks all the way.
The final thing to note is that we need to use the apolloReactHooksImportFrom
key if we want to use @apollo/client
version 3 beta, which we do, obviously.
Note: I have left in the required Hasura headers to show how you are able to add headers to the request, but you can ignore them if you are not using Hasura. If you are using Hasura, you are able to change the X-Hasura-Role
schema to generate GraphQL for different roles, which I think is really cool.
Our new workflow
When working with the backend now, we will solely be using .graphql
and the generated hooks. There are 3 types of GraphQL file we will make - Fragments, Queries and Mutations.
Naming convention
The way I like to name these files is:
[typeName].[mutationType?].[fragment | query | mutation].graphql
Examples:
image.fragment.graphql
post.fragment.graphql
post.query.graphql
posts.query.graphqluser.fragment.graphql
user.query.graphql
user.create.mutation.graphql
user.delete.mutation.graphql
Fragments
Fragments are reusable units of GraphQL Types. When you create a Fragment, you defining a set of fields that belong to a certain Type.
For each GraphQL Type you are going to be using in the front end, I’d like you to create a Fragment. This is because without creating a Fragment, Codegen will not generate & export an easy-to-use TypeScript Type, and instead just use an ugly TypeScript Omit. The below code will generate an ImageFragment
, a UserFragment
and a PostFragment
.
As you can see, you can “nest” fragments do minimise duplication, and to make them easier to work with in TypeScript. For example, with the fragments above, you may have an <Image />
component which accepts an ImageFragment
as a prop. If use the queries below, the data is fully typed throughout and you can pass in the user.avatar
, post.image
and post.featuredImage
straight to the <Image />
component, as TypeScript automatically recognises them as ImageFragment
s.
Queries and Mutations
Note: The format for your queries and mutations will obviously depend on your GraphQL server.
Queries are super easy to write in GraphQL, but if you do everything manually (as I did in my last project 😩), typing the Apollo query hooks can get rather annoying. So what’s the output of this when put through the Codegen?
Well, you get a bunch of Types which you can import, which is pretty handy. You also get hooks! usePostQuery()
and usePostLazyQuery()
which let you pass in the id
and out comes your typed data. Here’s an example component:
No need to pass the generics into the useQuery()
hook as that’s all done for you. data
is fully typed, and variables
are checked and have IntelliSense suggestions. All that from writing a very simple .graphql
file.
Other Benefits
- Backend and Frontend Sync: When you make a change to the backend, the Codegen can spot any breaking changes instantly. For example, changing the name of a field, changing the type of a field used in a mutation, or changing permissions so fields aren’t “visible” to that user, will throw an error instantly.
- Less typing means stronger Typing?!: The types are generated automatically from the GraphQL endpoint, which means you don’t make mistakes when writing the types. The less code you write, the less likely it is you will make a mistake!
- CI / CD: This can easily be integrated into your CI/CD workflow as an additional way to test and spot bugs in new environments on push.
- Monorepo: It’s easy to separate out this logic into a separate package in a monorepo to reuse your api client across multiple projects.
Conclusion
My aim when creating new apps is to make the code as simple and readable as possible, while keeping bugs to a minimum. This structure has really helped me in achieving that goal.
Please let me know in the comments if you have any questions or tips, and share if you think it is interesting or will help others 🙂