GraphQL is becoming common practice in the wild, while I feel the workflow with Typescript is still not straight forward. I want to propose one way to go about it and hopefully make your next Typescript GraphQL project a joy to work with!
Lets dive deeper 🤿.
I created a tiny companion repository if you want to check out the code and try it out.
Or check out the finished demo.
Intro
First we need to decide on what do we want (and probably need)
- Editor support for syntax highlighting
gql
and.graphql
files. - Strict type safety for our client.
- Easy tooling & workflow
So our workflow will look something like this:
GrapQL API -> Schema -> Queries & Mutations -> Typescript -> Client
For this article we’ll build a minuscule one pager using the SpaceX Land GraphQL API to display some space travel data.
Editor setup
The setup will be be for VSCode. For that we first install the GraphQL extension. This will enable us to have warnings and autocompletion inside of gql
tags and .graphql
files.
We need to add a .graphqlrc.yml
file at the root with the following content:
schema: https://api.spacex.land/graphql/
Writing Queries & Mutations
Now onto the real stuff.
We want to take our endpoint, generate types and queries from it that can then be used by Typescript safely. To do that we will:
- Setup generators for Schema, Queries, Mutations & SDK.
- Write some Queries & Mutations
- Generate the SDK
- Consume the SDK
Setup
There is this amazing project called @graphql-codegen
which is a collection of tools for helping you generating various things from GraphQL. Let’s install:
# Generators
pnpm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request
# For the SDK
pnpm i -D graphql graphql-request graphql-tag
I will assume my GraphQL stuff will live under ./src/lib/gql
We will create a top level configuration file to handle all of our generation step called codegen.yaml
. Ignore the config
option for now, I will explain that later
schema: https://api.spacex.land/graphql/
documents: 'src/**/*.graphql'
generates:
./src/lib/gql/gen.ts:
plugins:
- '@graphql-codegen/typescript'
- '@graphql-codegen/typescript-operations'
- '@graphql-codegen/typescript-graphql-request'
config:
maybeValue: 'T'
typesPrefix: GQL
immutableTypes: true
useTypeImports: true
avoidOptionals: true
The property schema
does not need an explanation.
generates
has 3 plugin enabled, one for the general types, another for queries and mutations and the last one to generate us a ready to use SDK and will save it under ./src/lib/gql/gen.ts
.
documents
is a glob that will find all our GraphQL files we write and generate the according code.
Creating Queries
Now let’s create a src/lib/gql/root.graphql
file and write some queries, all autocompleted of course!
query LaunchpadsMany {
launchpads(limit: 10) {
id
name
location {
name
}
successful_launches
status
}
}
query LaunchByYear($year: String!) {
launches(find: { launch_year: $year }) {
mission_id
mission_name
launch_date_utc
rocket {
rocket_name
}
}
}
Let magic do it’s thing
pnpm exec graphql-codegen
This will look at all our custom queries and mutations and generate us a ready to consume SDK that is completely typed. Amazing!
Leverage the new SDK
// src/lib/gql/index.ts
import { GraphQLClient } from 'graphql-request'
import { getSdk } from './gen'
const client = new GraphQLClient('https://api.spacex.land/graphql/')
export const SDK = getSdk(client)
import { SDK } from '$lib/gql'
const data = await SDK.LaunchByYear({ year: '2021' })
You can also use the generated types to explicitly set them
import { SDK } from '$lib/gql'
import type { GQLLaunchByYearQuery } from '$lib/gql/gen'
const data: GQLLaunchByYearQuery = await SDK.LaunchByYear({ year: '2021' })
Every thing is typed now, I can’t pass a number to the year
variable or use return data that does not exist. Typescript will error on me. This not only gives us autocompletion but also the safety of what we are doing.
Configuration options
I promised I would come back to it at some point.
schema: ...
generates:
...
config:
maybeValue: "T"
typesPrefix: GQL
immutableTypes: true
useTypeImports: true
avoidOptionals: true
There are many options for the generators, but I think these are quite sensible defaults.
maybeValue
is T | null
as default, but since we only use our queries which are type safe we can just remove uncertainty and use the correct type straight away.
avoidOptionals
same thing as the maybeValue
, just with prop:?
. Don’t want that.
typesPrefix
is useful if you have some own type specifications that you don’t want to clash with. I like to prefix all my generated GraphQL stuff with GQL
to keep it tidy.
immutableTypes
i prefer using an immutable type, with basically adds a readonly
to every property. This way we are sure we are not editing data on the client.
useTypeImports
this uses import type
whenever possible.
Final thoughts
I hope this made your GraphQL life a bit easier, it definitely did for me and it’s way more fun to consume GraphQL API this way. Also something worth mentioning is that you can use the @graphql-codegen/typescript-generic-sdk
package instead of the @graphql-codegen/typescript-graphql-request
if you want to do the network requests yourself. It’s easy to use but if you don’t really have a reason just stick with the graphql-request
one I’d say.