How to use GraphQL API with generated TypeScript Types?

Psst: we are hiring remote frontend developers and backend developers.

Illustration of a person holding a laptop and a person holding a picture of cat

When building modern applications, developers need to keep the code consistent, safe, and robust. They also want to reduce the feedback loop from the tools they are using, they want to catch mistakes as soon as possible, and types are key to achieving this. Due to the interpreted nature of Javascript (instead of compiled), TypeScript brings the type checking into the game for a safer runtime.

On top of that, GraphQL can also be “inconsistent” as the queries can be all different for each situation. TypeScript, with its powerful Type system (and Utility Types), can bring consistency and robustness by reusing the GraphQL schema. You can compose as many subtypes as you want to match your GraphQL queries.

GraphQL APIs use schema definition language (SDL) to describe all types. You should still make sure your client application does not contain any type errors. There is a GraphQL Code Generator for converting GraphQL schema to a TypeScript file with all type definitions for your client application. 

[newsletter]Join Crystallize newsletter!

Learn, build and grow your headless commerce with tips and tricks delivered to your inbox!

Benefits from Generated GraphQL Types

GraphQL API validates all requests and returns schema-valid responses only (documented here). However, the client application still needs to send a valid request and correctly consume response data. In JavaScript (or non-typed TypeScript) code, you can easily have typographical mistakes or access non-existing properties. You should prevent the application from these types of errors. You can test the application manually, but I advise you to use GraphQL code generators. 

GraphQL code generators help generate corresponding request input and output types, and typically these generators also validate your query strings. You will get extra validation for your existing API calls. Integrating new queries and their variables will be easier.

[note]Note.

There are a few considerations. Ideally, you should not include generated sources in the repository, and you should set up a workflow to keep your generated sources up to date. Therefore, introducing a GraphQL code generator means more complexity to your development setup and possibly onboarding process, especially with robust applications and less experienced developers.

The following section describes how to configure the GraphQL code generator for TypeScript. However, there are ways to generate types for other languages, for example, Kotlin, etc.

Configuration

First of all, you need to add all these developer dependencies:

You can do that by running a single command: 

yarn add --dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/add

In the meantime, you can start defining your code generation configuration by creating codegen.yml: 

./src/generated/crystallize-orders-gql-types.ts:
schema:
- https://api.crystallize.com/${TENANT_IDENTIFIER}/orders:
headers:
X-Crystallize-Access-Token-Id: ${CRYSTALLIZE_TOKEN_ID}
X-Crystallize-Access-Token-Secret: ${CRYSTALLIZE_TOKEN_SECRET}
documents:
- src/services/crystallize/graphql-queries/admin-api.ts
plugins:
- typescript
- typescript-operations
- add:
content: >
/**
* THIS IS GENERATED FILE. DO NOT MODIFY IT DIRECTLY, RUN 'yarn gen:types' INSTEAD.
*/

This configuration will generate types for Crystallize order API for tenant specified by environment variable TENANT_IDENTIFIER, for example, listed in the .env file. As you can see, the configuration contains an NPM script yarn gen:types. So don’t forget to create also the script with the following command: 

graphql-codegen -r dotenv/config

The last thing you want to do is exclude this generated source from Git tracking, code analysis, and formatting. Your ignore file lists (.gitignore, etc.) should contain these generated sources. 

The configuration contains a plugin to add a warning comment, and also there is a plugin for typescript-operations. This plugin checks all queries and mutations and generates the corresponding TS type: 

import { gql } from 'apollo-server-micro'

export const CREATE_ORDER = gql`
mutation CreateOrder($input: CreateOrderInput!) {
orders {
create(input: $input) {
id
}
}
}
`

// ...generates:

export type CreateOrderMutation = { __typename?: 'Mutation' } & {
orders: { __typename?: 'OrderMutations' } & {
create: { __typename?: 'OrderConfirmation' } & Pick<OrderConfirmation, 'id'>
}
}

export type CreateOrderMutationVariables = Exact<{
input: CreateOrderInput
}>

Crystallize order API has an authentication layer like many other APIs. It is necessary to generate a personal access token and paired ID to use all Crystallize APIs with a higher security level. The code generator configuration includes a section with HTTP headers. You should avoid keeping security credentials inside the Git repository. It is the reason why these HTTP headers are not present in the configuration, and they are accessible via environment variables.

More Solid Service API with Crystallize

The best place to use this tool with Crystallize is your service API. It is the center of your eCommerce, and all functions need to be 100% secure and error-free. Setup for subscriptions API and PIM API will be similar to orders in the previous section. Besides that, your service API will probably need to contain a transformation layer between Crystallize API and your business logic. 

Crystallize API is an open API. For example, each order contains a field for additionalInformation. The property is nullable, and there can be a case where this field is not in use at all. However, you can have a use case where this field will be mandatory. In this case, your transformation layer will validate the field value and eventually handle an error case: 

const getOrder = async (id: string): Promise<MyOrder | null> => {
const response = await orderApiQuery<GetOrderQuery, GetOrderQueryVariables>({
query: GET_ORDER,
variables: { id },
})

const { data, errors } = response

if (!data || errors) {
return null
}

return transformToMyOrder(data)
}

function transformToMyOrder(query: GetOrderQuery): MyOrder | null {
const { order: { get: order } } = query

const { id, additionalInformation } = order

if (!additionalInformation) {
return null
}

return { id, additionalInformation }
}

interface MyOrder {
id: string
additionalInformation: string
}

export const GET_ORDER = gql`
mutation GetOrder($id: ID!) {
orders {
get(id: $id) {
id
additionalInformation
}
}
}
`

Last Few Words

Integrating this setup is a returnable investment. It will save you from a couple of bugs. For example, you will not see the "can't read property of null" error or API error response due to a missing field in input anymore. By the end, your eCommerce frontends will be more stable with more happy customers, and your business will be more successful.