|
| 1 | +--- |
| 2 | +title: How We Implemented Type-Safe GraphQL With Hasura and the graphql-codegen |
| 3 | + CLI Tool |
| 4 | +author: Aaron Porter |
| 5 | +authorTitle: Software Engineer |
| 6 | +date: 2023-12-19T15:32:38.270Z |
| 7 | +description: TBD |
| 8 | +--- |
| 9 | +In the last few months, the Catalog Engineering team has been hard at work on a new app to improve our customer's course curation process. This app connects to our new Hasura GraphQL API, to which we make requests for, and to mutate, data. Early on, we made the decision to not use a dedicated GraphQL client (e.g., apollo) and so we got used to creating TypeScript types for our GraphQL requests/responses as we needed them. This worked fine in the beginning as our app only had a few simple requests. However, as the number and complexity of these requests/responses grew, it became apparent that we needed a better solution. |
| 10 | + |
| 11 | +Enter [graphql-codegen](https://the-guild.dev/graphql/codegen) - a CLI tool that allows you to generate typed queries, mutations, subscriptions, and more based on your GraphQL schema. With graphql-codegen we were able to remove all of our manual types and generate new (and better!) types on the fly. This post will walk through how we connected the tool to our Hasura GraphQL schema, how we're able to make use of those types, and walkthrough setting up a custom 'SDK' that makes calling our methods a breeze. Let's get started! |
| 12 | + |
| 13 | +## Installation |
| 14 | + |
| 15 | +Note: _the following steps assume you have a working TypeScript app that is connected to an established Hasura GraphQL API with at least one GraphQL query/mutation/subscription_ |
| 16 | + |
| 17 | +First, you'll need to install the tool and a few dependencies. Run the following: |
| 18 | + |
| 19 | +```shell |
| 20 | +npm i graphql graphql-config |
| 21 | +``` |
| 22 | + |
| 23 | +And a few dev dependencies (we'll talk a bit more about some of these later): |
| 24 | + |
| 25 | +```shell |
| 26 | +npm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-generic-sdk |
| 27 | +``` |
| 28 | + |
| 29 | +## Configuration |
| 30 | + |
| 31 | +Next, create a GraphQL config file (detailed information on `graphql-config` can be found [here](https://the-guild.dev/graphql/config/docs)): |
| 32 | + |
| 33 | +```typescript |
| 34 | +// graphql.config.ts |
| 35 | + |
| 36 | +import type { IGraphQLConfig } from 'graphql-config'; |
| 37 | +import { loadEnv } from 'vite'; |
| 38 | + |
| 39 | +const { HASURA_ADMIN_SECRET, VITE_GRAPHQL_API_URL } = loadEnv('', process.cwd(), ''); |
| 40 | + |
| 41 | +const config: IGraphQLConfig = { |
| 42 | + schema: [ |
| 43 | + { |
| 44 | + [VITE_GRAPHQL_API_URL]: { |
| 45 | + headers: { |
| 46 | + 'x-hasura-admin-secret': HASURA_ADMIN_SECRET, |
| 47 | + }, |
| 48 | + }, |
| 49 | + }, |
| 50 | + ], |
| 51 | + documents: 'src/**/*.graphql', |
| 52 | + extensions: { |
| 53 | + codegen: { |
| 54 | + overwrite: true, |
| 55 | + generates: { |
| 56 | + 'src/__generated__/graphql.ts': { |
| 57 | + plugins: ['typescript', 'typescript-operations', 'typescript-generic-sdk'], |
| 58 | + config: { |
| 59 | + skipTypename: true, |
| 60 | + namingConvention: { |
| 61 | + typeNames: 'change-case-all#pascalCase', |
| 62 | + enumValues: 'change-case-all#upperCase', |
| 63 | + }, |
| 64 | + rawRequest: true, |
| 65 | + scalars: { |
| 66 | + uuid: 'string', |
| 67 | + }, |
| 68 | + }, |
| 69 | + }, |
| 70 | + }, |
| 71 | + }, |
| 72 | + }, |
| 73 | +}; |
| 74 | + |
| 75 | +export default config; |
| 76 | + |
| 77 | +``` |
| 78 | + |
| 79 | +A few notes about the above config: |
| 80 | + |
| 81 | +- We are using Vite as our build tool. We import several environment variables via Vite's `loadEnv`. The `schema` config will be the same for you - just pop in your Hasura GraphQL API URL and admin secret and you should be set |
| 82 | +- Plugins allows for tons of custom configuration. We've chosen several customization that work well for us, but you might choose something differe. Also, make note of the `typescript-generic-sdk` plugin - we'll talk more about that later |
| 83 | + |
| 84 | +## An Example Mutation |
| 85 | + |
| 86 | +Let's quickly walk through the example mutation that we'll be reference moving forward. Our Hasura GraphQL schema has a table called `users`, so we'll need a way to add new `users`. In order to do that, we'll use the following mutation: |
| 87 | + |
| 88 | +```graphql |
| 89 | +# src/graphql/createUser.graphql |
| 90 | + |
| 91 | +mutation createUser($organization_id: String, $user_id: uuid) { |
| 92 | + insert_user_detail_one(object: { organization_id: $organization_id, id: $user_id }) { |
| 93 | + id |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +As you can see, this mutation takes an `organization_id` and `user_id` and returns the user's `id` |
| 99 | + |
| 100 | +## Generating Types |
| 101 | + |
| 102 | +Now that we have our mutation and the tool is configured, let's see what our generated output looks like. We can run `npm run graphql-codegen` to kick that off |
| 103 | + |
| 104 | +Once finished, you should see `src/__generated__/graphql.ts`. Looking inside, we can see types for the mutation and the mutation's variables: |
| 105 | + |
| 106 | + |
| 107 | + |
| 108 | +as well as a `const` for the GraphQL document: |
| 109 | + |
| 110 | + |
| 111 | + |
| 112 | +Pretty cool, right?! Now, how can we make use of these types? Since we're not using something like Apollo, we'd need to wire up our Hasura requester to handle our newly generated mutation variables, return types, and the GraphQL document - that sounds like a lot of work, right?! This is where we can make use of that 'SDK' mentioned earlier |
| 113 | + |
| 114 | +## Putting Everything Together |
| 115 | + |
| 116 | +If we continue searching in `graphql.ts`, we'll see that we also have a type for a requester and an 'SDK' which has everything neatly typed and ready to go: |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +All we need to do at this point is to implement the requester and we should be able to call the `createUser` method from the 'SDK'. That might look something like this: |
| 121 | + |
| 122 | +```typescript |
| 123 | +// hasuraRequester.ts |
| 124 | + |
| 125 | +import { DocumentNode, ExecutionResult } from 'graphql'; |
| 126 | +import { getSdk } from '../__generated__/graphql'; |
| 127 | + |
| 128 | +const requester = async <R, V>(doc: DocumentNode, variables?: V, options?: { jwt: string }) => { |
| 129 | + const requestBody = { query: doc.loc?.source.body, variables }; |
| 130 | + const requestOptions = { |
| 131 | + method: 'POST', |
| 132 | + headers: { |
| 133 | + 'Content-Type': 'application/json', |
| 134 | + Authorization: `Bearer ${options?.jwt}`, |
| 135 | + }, |
| 136 | + body: JSON.stringify(requestBody), |
| 137 | + }; |
| 138 | + |
| 139 | + const response = await fetch(import.meta.env.HASURA_API_URL, requestOptions); |
| 140 | + return (await response.json()) as ExecutionResult<R, unknown>; |
| 141 | +}; |
| 142 | + |
| 143 | +const sdk = getSdk(requester); |
| 144 | + |
| 145 | +const createHasuraUser = (userId: string, orgId: string) => { |
| 146 | + return sdk.createUser({ organization_id: orgId, user_id: userId }); |
| 147 | +}; |
| 148 | + |
| 149 | +export default createHasuraUser; |
| 150 | +``` |
| 151 | + |
| 152 | +If we import the 'SDK', we should now have typing around calling the `createHasuraUser` method: |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +and we see the typed response: |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | +and if we keep going, we also see the `id` mentioned earlier would be available to us |
| 161 | + |
| 162 | + |
0 commit comments