From 60c9eeed9c59c007980285ebdbf8a0f44eed85ca Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 10:30:13 +0200 Subject: [PATCH 01/13] Add Codegen blog post --- src/pages/blog/2024-09-18-codegen.mdx | 167 ++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/pages/blog/2024-09-18-codegen.mdx diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx new file mode 100644 index 0000000000..5374c53a2b --- /dev/null +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -0,0 +1,167 @@ +--- +title: "Generating type safe clients using code generation" +tags: ["blog"] +date: 2024-09-18 +byline: Martin Bonnin +--- + +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. + +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. + +By using code generation, you get: + +- compile time guarantees about your code and the data it manipulates. +- Autocomplete and inline documentation in your favorite IDE. + +All of that without having to write and maintain types manually! + +For simplicity, this post uses TypeScript for code blocks but the same concepts can be applied to Swift/Kotlin. + +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: + +## Problem: Generating code from schema types loses nullability information + +Let's assume this schema: + +```graphql +type Product { + id: String! + """ + The name of the product. + A product must always have a name. + """ + name: String! + """ + The description of the product. + May be null if the product doesn't have a description. + """ + description: String + """ + The price of the product. + May be null if the product doesn't have a description. + """ + price: Float +} + + +type Query { + products: [Product!]! +} +``` + +A translation to TypeScript might yield the following: + +```typescript +// First attempt at generating code from the product type +type Product = { + id: string; + name: string; + description: string | null; + price: string | null +} +``` + +Pretty neat, right? Typescript and GraphQL look really similar... Unfortunately, this is not type safe! + +Let's do a query that doesn't query the product name: + +```graphql +query GetProduct { + products { + id + # no name here + description + price + } +} +``` + +Returned product: + +```json +{ + "id": "42", + "description": null, + "price": 15.5 +} +``` + +It's now impossible to map that returned value to our type because `name` must be non-null. + +We can also apply aliases: + +```graphql +query GetProduct { + products { + id + productName: name + description + price + } +} +``` + +Returned product: + +```json +{ + "id": "42", + "productName": "My Product", + "description": null, + "price": 15.5 +} +``` + +Note that the `productName`, despite being non-null, does not match up with the expected `name` field. + +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! + +Thankfully, we can solve this by generating code based on operations instead (queries, mutations, and subscriptions). + +## Solution: Generating code from GraphQL operations + +By generating code from GraphQL operations, we are now certain that the TypeScript fields always represent GraphQL fields that have been requested. + +Reusing our first example: + +```graphql +query GetProduct { + products { + id + price + } +} +``` + +Typescript: + +```typescript +type GetProductData = { + products: Array +} + +type GetProductData_products = { + # Only the fields appearing in the query appear in the generated type + id: string; + price: string | null +} +``` + +With this `Product` type: + +* `name` is not present in the generated type because it was not queried. +* `id` is not-nullable, as intended. A product always has an `id`. +* `price` is nullable, as intended. If `null`, it means the product doesn't have a price. + +This is what we expected! + +## Conclusion + +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. + +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. + +If you are not already, try them out! + +And look out for a new post soon on the improvements we're hoping to bring to GraphQL nullability! \ No newline at end of file From 33244eedcfad5ebb35c5344d7627431f4538f222 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:03:23 +0200 Subject: [PATCH 02/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 5374c53a2b..24a9d96a7f 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -11,8 +11,8 @@ If you're not using code generation, you're missing out on a lot, especially if By using code generation, you get: -- compile time guarantees about your code and the data it manipulates. -- Autocomplete and inline documentation in your favorite IDE. +- compile time guarantees about your code and the data it manipulates, and +- autocomplete and inline documentation in your favorite IDE. All of that without having to write and maintain types manually! From dc61d7193b95f72efbb97a88a4aa041708f7b842 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:03:30 +0200 Subject: [PATCH 03/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 24a9d96a7f..199be15ba4 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -64,7 +64,7 @@ type Product = { Pretty neat, right? Typescript and GraphQL look really similar... Unfortunately, this is not type safe! -Let's do a query that doesn't query the product name: +Let's perform a query that doesn't request the product name: ```graphql query GetProduct { From bda5b28e48c5aabbd89c3b6ff2bdc2279abf1ccf Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:22 +0200 Subject: [PATCH 04/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 199be15ba4..ab30fb07fa 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -129,6 +129,7 @@ Reusing our first example: query GetProduct { products { id + description price } } From 84b0efa9207e7a787e3a5fee5632f85b5ce86367 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:28 +0200 Subject: [PATCH 05/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index ab30fb07fa..495ee9afd5 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -145,6 +145,7 @@ type GetProductData = { type GetProductData_products = { # Only the fields appearing in the query appear in the generated type id: string; + description: string | null; price: string | null } ``` From ef02a64650ac420d2dd70ab55fb440fa431add1c Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:34 +0200 Subject: [PATCH 06/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 495ee9afd5..8c8a194b3d 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -143,7 +143,6 @@ type GetProductData = { } type GetProductData_products = { - # Only the fields appearing in the query appear in the generated type id: string; description: string | null; price: string | null From e68ca4ce7bd7fce9f49b6983b3ff9d10e27df9f1 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:41 +0200 Subject: [PATCH 07/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 8c8a194b3d..010c4f0403 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -138,6 +138,8 @@ query GetProduct { Typescript: ```typescript +# Only the fields appearing in the `GetProduct` query appear in the generated types + type GetProductData = { products: Array } From d1d617ca6e9f1aeff959c8c502a4ba900f13f846 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:46 +0200 Subject: [PATCH 08/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 010c4f0403..fcd6ed290d 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -151,7 +151,7 @@ type GetProductData_products = { } ``` -With this `Product` type: +With this `GetProductData_products` type: * `name` is not present in the generated type because it was not queried. * `id` is not-nullable, as intended. A product always has an `id`. From 0827ea54b3163f6498ed9632ab6937aab901ca71 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:53 +0200 Subject: [PATCH 09/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index fcd6ed290d..7897de2791 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -155,7 +155,7 @@ With this `GetProductData_products` type: * `name` is not present in the generated type because it was not queried. * `id` is not-nullable, as intended. A product always has an `id`. -* `price` is nullable, as intended. If `null`, it means the product doesn't have a price. +* `description` and `price` are nullable, as intended. If `null`, it means the product doesn't have a description/price. This is what we expected! From 6b09c368b974f904f0382700e51a15e63adfb8a3 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Wed, 18 Sep 2024 14:04:58 +0200 Subject: [PATCH 10/13] Update src/pages/blog/2024-09-18-codegen.mdx Co-authored-by: Benjie --- src/pages/blog/2024-09-18-codegen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 7897de2791..0d9cf7b437 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -165,6 +165,6 @@ By using code generation based on operations, you get type safety from your back 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. -If you are not already, try them out! +If you have not already, try them out! And look out for a new post soon on the improvements we're hoping to bring to GraphQL nullability! \ No newline at end of file From 0cfb74135efe347e2c7400dd14525606d0ae934f Mon Sep 17 00:00:00 2001 From: Benjie Date: Wed, 18 Sep 2024 14:22:26 +0100 Subject: [PATCH 11/13] Update src/pages/blog/2024-09-18-codegen.mdx --- src/pages/blog/2024-09-18-codegen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 0d9cf7b437..0b43703ed9 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -135,7 +135,7 @@ query GetProduct { } ``` -Typescript: +TypeScript: ```typescript # Only the fields appearing in the `GetProduct` query appear in the generated types From e23b8f9a05f9fce4ff9d5bdeb31fcfbe58adb9e5 Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 19 Sep 2024 18:27:21 +0100 Subject: [PATCH 12/13] Update src/pages/blog/2024-09-18-codegen.mdx --- src/pages/blog/2024-09-18-codegen.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-18-codegen.mdx index 0b43703ed9..30bf3a43d8 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-18-codegen.mdx @@ -1,7 +1,7 @@ --- title: "Generating type safe clients using code generation" tags: ["blog"] -date: 2024-09-18 +date: 2024-09-19 byline: Martin Bonnin --- From 2a6ee4f392f667b3926af65c3231229695c80589 Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 19 Sep 2024 18:27:34 +0100 Subject: [PATCH 13/13] Rename 2024-09-18-codegen.mdx to 2024-09-19-codegen.mdx --- .../blog/{2024-09-18-codegen.mdx => 2024-09-19-codegen.mdx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/pages/blog/{2024-09-18-codegen.mdx => 2024-09-19-codegen.mdx} (99%) diff --git a/src/pages/blog/2024-09-18-codegen.mdx b/src/pages/blog/2024-09-19-codegen.mdx similarity index 99% rename from src/pages/blog/2024-09-18-codegen.mdx rename to src/pages/blog/2024-09-19-codegen.mdx index 30bf3a43d8..83aee20e5e 100644 --- a/src/pages/blog/2024-09-18-codegen.mdx +++ b/src/pages/blog/2024-09-19-codegen.mdx @@ -167,4 +167,4 @@ All the major code generators ([graphql-code-generator](https://github.com/dotan If you have not already, try them out! -And look out for a new post soon on the improvements we're hoping to bring to GraphQL nullability! \ No newline at end of file +And look out for a new post soon on the improvements we're hoping to bring to GraphQL nullability!