Skip to content

Commit e61c536

Browse files
committed
Create Blog “how-we-implemented-type-safe-graphql-with-hasura-and-the-graphql-codegen-cli-tool/index”
1 parent 79cad80 commit e61c536

File tree

1 file changed

+162
-0
lines changed
  • content/blog/how-we-implemented-type-safe-graphql-with-hasura-and-the-graphql-codegen-cli-tool

1 file changed

+162
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
![Generated types for the mutation's return type and variables](mutation_return_type_and_variables.png)
107+
108+
as well as a `const` for the GraphQL document:
109+
110+
![An exported const of the GraphQL document](mutation_document.png)
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+
![Types for a generic requester and an SDK with a method called createUser that makes use of the associated types](generic_sdk_with_mutation.png)
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+
![The type of the createHasuraUser method](requester_typed_method.png)
155+
156+
and we see the typed response:
157+
158+
![The typed response from Hasura](requester_typed_response.png)
159+
160+
and if we keep going, we also see the `id` mentioned earlier would be available to us
161+
162+
![The typed payload from our GraphQL mutation](requester_typed_response_data.png)

0 commit comments

Comments
 (0)