Skip to content

Commit 67c5121

Browse files
martinbonninbenjie
andauthored
Add Codegen blog post (#1778)
Co-authored-by: Benjie <[email protected]>
1 parent ae6e0ec commit 67c5121

File tree

1 file changed

+170
-0
lines changed

1 file changed

+170
-0
lines changed

src/pages/blog/2024-09-19-codegen.mdx

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
title: "Generating type safe clients using code generation"
3+
tags: ["blog"]
4+
date: 2024-09-19
5+
byline: Martin Bonnin
6+
---
7+
8+
A GraphQL endpoint usually returns a JSON payload. While you can use the result as a dynamic object, the GraphQL type system gives us a lot of information about what is inside that JSON payload.
9+
10+
If you're not using code generation, you're missing out on a lot, especially if you're using a type-safe language such as TypeScript, Swift or Kotlin.
11+
12+
By using code generation, you get:
13+
14+
- compile time guarantees about your code and the data it manipulates, and
15+
- autocomplete and inline documentation in your favorite IDE.
16+
17+
All of that without having to write and maintain types manually!
18+
19+
For simplicity, this post uses TypeScript for code blocks but the same concepts can be applied to Swift/Kotlin.
20+
21+
A common mistake is to attempt to use the GraphQL schema directly for type generation, but this is not type-safe since GraphQL only returns the fields that you ask for, and allows you to alias fields in the response. Instead, types should be generated based on the GraphQL operations (requests) that you issue. Here's an illustration of the issue:
22+
23+
## Problem: Generating code from schema types loses nullability information
24+
25+
Let's assume this schema:
26+
27+
```graphql
28+
type Product {
29+
id: String!
30+
"""
31+
The name of the product.
32+
A product must always have a name.
33+
"""
34+
name: String!
35+
"""
36+
The description of the product.
37+
May be null if the product doesn't have a description.
38+
"""
39+
description: String
40+
"""
41+
The price of the product.
42+
May be null if the product doesn't have a description.
43+
"""
44+
price: Float
45+
}
46+
47+
48+
type Query {
49+
products: [Product!]!
50+
}
51+
```
52+
53+
A translation to TypeScript might yield the following:
54+
55+
```typescript
56+
// First attempt at generating code from the product type
57+
type Product = {
58+
id: string;
59+
name: string;
60+
description: string | null;
61+
price: string | null
62+
}
63+
```
64+
65+
Pretty neat, right? Typescript and GraphQL look really similar... Unfortunately, this is not type safe!
66+
67+
Let's perform a query that doesn't request the product name:
68+
69+
```graphql
70+
query GetProduct {
71+
products {
72+
id
73+
# no name here
74+
description
75+
price
76+
}
77+
}
78+
```
79+
80+
Returned product:
81+
82+
```json
83+
{
84+
"id": "42",
85+
"description": null,
86+
"price": 15.5
87+
}
88+
```
89+
90+
It's now impossible to map that returned value to our type because `name` must be non-null.
91+
92+
We can also apply aliases:
93+
94+
```graphql
95+
query GetProduct {
96+
products {
97+
id
98+
productName: name
99+
description
100+
price
101+
}
102+
}
103+
```
104+
105+
Returned product:
106+
107+
```json
108+
{
109+
"id": "42",
110+
"productName": "My Product",
111+
"description": null,
112+
"price": 15.5
113+
}
114+
```
115+
116+
Note that the `productName`, despite being non-null, does not match up with the expected `name` field.
117+
118+
We simply cannot safely use the schema types to represent requests unless we fetch every single field on every single type, which would go against GraphQL's very nature!
119+
120+
Thankfully, we can solve this by generating code based on operations instead (queries, mutations, and subscriptions).
121+
122+
## Solution: Generating code from GraphQL operations
123+
124+
By generating code from GraphQL operations, we are now certain that the TypeScript fields always represent GraphQL fields that have been requested.
125+
126+
Reusing our first example:
127+
128+
```graphql
129+
query GetProduct {
130+
products {
131+
id
132+
description
133+
price
134+
}
135+
}
136+
```
137+
138+
TypeScript:
139+
140+
```typescript
141+
# Only the fields appearing in the `GetProduct` query appear in the generated types
142+
143+
type GetProductData = {
144+
products: Array<GetProductData_products>
145+
}
146+
147+
type GetProductData_products = {
148+
id: string;
149+
description: string | null;
150+
price: string | null
151+
}
152+
```
153+
154+
With this `GetProductData_products` type:
155+
156+
* `name` is not present in the generated type because it was not queried.
157+
* `id` is not-nullable, as intended. A product always has an `id`.
158+
* `description` and `price` are nullable, as intended. If `null`, it means the product doesn't have a description/price.
159+
160+
This is what we expected!
161+
162+
## Conclusion
163+
164+
By using code generation based on operations, you get type safety from your backend all the way to your UI. On top of that, your IDE can use the generated code to provide autocomplete and a better experience overall.
165+
166+
All the major code generators ([graphql-code-generator](https://github.com/dotansimha/graphql-code-generator), [Relay](https://relay.dev/), [Apollo iOS](https://github.com/apollographql/apollo-ios), [Apollo Kotlin](https://github.com/apollographql/apollo-kotlin), ...) generate code based on operations.
167+
168+
If you have not already, try them out!
169+
170+
And look out for a new post soon on the improvements we're hoping to bring to GraphQL nullability!

0 commit comments

Comments
 (0)