A mini application built with Next.js and Hasura to demonstrate use of tools that enhance TypeScript experience with GraphQL.
- Next.js
- Hasura
- GraphQL Codegen
- React Query
- graphql-ws
- Zod
- ts-to-zod
When a query, a mutation or a subscription changes in the codebase and is defined as a parameter to graphql
function, types are automatically regenerated.
If the query returns different fields than before, TypeScript would tell us. And TypeScript would also ensure expected variables are properly provided, both the name of the variable and its type.
Ensure that the server really returns what we expect from it, or throw.
If the schema defined on the server has been changed and the application not redeployed, it would throw an error at runtime. These errors can be caught and reported.
Write queries without leaving your text editor to check documentation.
It even completes complex queries, like those generated by Hasura (like where
or order_by
clauses).
The generation of types is managed by GraphQL Codegen. This tool has a lot of plugins, that can generate different things, from types for a whole GraphQL schema, to custom Apollo hooks for your queries/mutations/subscriptions.
We use client preset, that does several things for us:
- Generate TypeScript types for all types, inputs, enums, queries, and more, defined inside GraphQL schema
- Generate types for queries/mutations/subscriptions found in your front-end codebase
- Generate a
graphql
function that takes a query/mutation/subscription and returns aTypedDocumentNode
, that allows to statically know its result, and the parameters it must take; all queries must now be wrapped within thisgraphql
function to get type inference
See files generated by GraphQL Codegen →
GraphQL Codegen can be started one-off:
yarn codegen:graphql
Or wait for changes in the codebase:
yarn codegen:graphql:watch
To ensure queries and mutations are executed with required variables, and to get their result type, we can use makeGqlRequest
. It takes a TypedDocumentNode
as parameter, and determines the variables it requires with VariablesOf
generic from @graphql-typed-document-node/core
, and uses ResultOf
to get the result of the operation.
For subscriptions, we can use useSubscription
hook, and listen to updates from server thanks to graphql-ws
library. It leverages the same generics as for makeGqlRequest
.
To validate responses from server, we use Zod. Generally, people use Zod to create validators and derive from them types. Here we already have types, and we want to get Zod validators. We can use ts-to-zod tool to do that.
ts-to-zod can be started one-off:
yarn ts-to-zod
It is configured to generate validators only for types of queries, mutations and subscriptions, which must have been generated previously by GraphQL Codegen, and to put them in ./gql/graphql.zod.ts
.
To take benefit from validators generated by ts-to-zod, makeGqlRequest
and useSubscription
functions need to execute the validator that matches the request being made.
To do that, we need to find the name of the operation inside the AST of the query. graphql
functions returns a TypedDocumentNode
, that is, at runtime, an AST representation of the query.
Say we are executing makeGqlRequest
for GetArticleById
query, defined in pages/article/[id].tsx
:
async function getArticleById(id: string) {
const result = await makeGqlRequest(
graphql(`
query GetArticleById($id: uuid!) {
article: news_feeds_by_pk(uuid: $id) {
uuid
title
text
news_comments(order_by: { created_at: desc }) {
uuid
text
created_at
}
created_at
}
}
`),
{ id }
);
return result;
}
graphql
function returns an AST representation of the query, generated by GraphQL Codegen, we can find at the bottom of gql/graphql.ts:
export const GetArticleByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetArticleById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"uuid"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"article"},"name":{"kind":"Name","value":"news_feeds_by_pk"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"uuid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"news_comments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"order_by"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"created_at"},"value":{"kind":"EnumValue","value":"desc"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"created_at"}}]}},{"kind":"Field","name":{"kind":"Name","value":"created_at"}}]}}]}}]} as unknown as DocumentNode<GetArticleByIdQuery, GetArticleByIdQueryVariables>;
We can see that the name of the operation is available at: document.definitions[0].name.value
, and evaluates to GetArticleById
. And the type of the operation (query/mutation/subscription) is at: document.definitions[0].operation
, and evaluates to query
.
The Zod validator we want to use, generated by ts-to-zod, is called GetArticleByIdQuery
. So, by merging the operation name and the operation type, we can import the validator we want from ./gql/graphql.zod.ts
, and parse the result of the server with it.
GraphQL VSCode extension provides autocompletion for queries you write.
It needs the GraphQL server of your server, and in which files autocompletion should be activated. This is configured in .graphqlrc.yml
:
schema: ./schema.graphql
documents: pages/**/*.tsx
To get the GraphQL schema of the server, we can use another plugin of GraphQL Codegen: schema-ast.
Other tools are meant to bring typesafety in other configurations:
- tRPC: End-to-end typesafe API without type generation; requires control over the server
- openapi-zod-client: Generate Zod validators from OpenAPI specification
- Type safety from Hasura to SWR – Frontend First
- The idea of combining the tools used in this mini application comes from this video
- Ryan Toronto helped me to set up ts-to-zod