Your Guide to GraphQL with TypeScript
- Why Type Safety
- Why isn’t everyone embracing type-safe languages?
- Tutorial
- Tutorial 2 (using GraphQL Code Generator)
Why Type Safety
- Callsite safety
- Improved developer efficiency
- Integrated documentation
- Enhanced workflow
Why isn’t everyone embracing type-safe languages?
Tutorial
Stack
Defining “Generative”
Prerequisites:
Step One: Clone and configure the code repository
- A) Use
docker compose
to run a local image of Hasura GraphQL Engine - B) Swap the URL value at
typescript-gql-workshop-demo/utils/constants.ts
fromlocalhost:8080
to your Hasura Cloud app's URL
$ docker-compose up -d # Start Hasura + Postgres in Docker
$ cd hasura
$ hasura migrate apply # Apply DB table migrations
$ hasura metadata apply # Apply metadata for tables
$ cd ../
$ yarn install
$ yarn dev
# Now visit http://localhost:3000
export const CONSTANTS = {
HASURA_URL: "https://my.hasura.app"
}
$ cd hasura
$ hasura migrate apply --endpoint https://my.hasura.app --admin-secret secret # Apply DB table migrations
$ hasura metadata apply --endpoint https://my.hasura.app --admin-secret secret # Apply metadata for tables
$ cd ../
$ yarn install
$ yarn dev
# Now visit http://localhost:3000
Step Two: Run Hasura Console
$ cd hasura
# With Hasura OSS:
$ hasura console
# Or, if you're using Hasura Cloud:
$ hasura console --endpoint https://my.hasura.app --admin-secret secret
Step Three: Generate the typings
yarn generate-gql-client
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"post-update": "yarn upgrade --latest",
"generate-gql-client": "zeus http://localhost:8080/v1/graphql ./utils/generated --ts"
}
}
- Run
graphql-zeus
on the API that exists atlocalhost:8080
- Place the generated file(s) in the directory
./utils/generated
- Use TypeScript (
--ts
) for the SDK. (Alternatives:JS
,ESM
)
-H 'X-Hasura-Admin-Secret: mysecret
Step Four: Consume the SDK
import { Gql } from "./graphql-zeus"
export const Gql = Chain('http://localhost:8080/v1/graphql')
export const Chain = (...options: fetchOptions) => ({
query: ((o: any, variables) =>
fullChainConstruct(apiFetch(options))('query', 'query_root')(o, variables).then(
(response: any) => response
)) as OperationToGraphQL<ValueTypes["query_root"],query_root>,
mutation: ((o: any, variables) =>
fullChainConstruct(apiFetch(options))('mutation', 'mutation_root')(o, variables).then(
(response: any) => response
)) as OperationToGraphQL<ValueTypes["mutation_root"],mutation_root>,
subscription: ((o: any, variables) =>
fullChainConstruct(apiFetch(options))('subscription', 'subscription_root')(o, variables).then(
(response: any) => response
)) as OperationToGraphQL<ValueTypes["subscription_root"],subscription_root>
});
query {
user(where: {
username: { _eq: "person " }
}) {
id
username
user_todos {
todo {
id
description
is_completed
}
}
}
}
const result = Gql.query({
user: [
{
where: {
username: { _eq: "person " },
},
},
{
id: true,
username: true,
user_todos: [
{},
{
todo: {
id: true,
description: true,
is_completed: true,
},
},
],
},
],
})
import {
ApolloClient,
gql,
LazyQueryHookOptions,
MutationHookOptions,
MutationOptions,
NormalizedCacheObject,
QueryHookOptions,
QueryOptions,
SubscriptionHookOptions,
SubscriptionOptions,
useLazyQuery,
useMutation,
useQuery,
useSubscription,
} from "@apollo/client"
import {
MapType,
mutation_root,
query_root,
Selectors,
subscription_root,
ValueTypes,
Zeus,
} from "./generated/graphql-zeus"
export function useTypedQuery<Q extends ValueTypes["query_root"]>(
query: Q,
options?: QueryHookOptions<MapType<query_root, Q>, Record<string, any>>,
) {
return useQuery<MapType<query_root, Q>>(gql(Zeus.query(query)), options)
}
export function useTypedLazyQuery<Q extends ValueTypes["query_root"]>(
query: Q,
options?: LazyQueryHookOptions<MapType<query_root, Q>, Record<string, any>>,
) {
return useLazyQuery<MapType<query_root, Q>>(gql(Zeus.query(query)), options)
}
export function useTypedMutation<Q extends ValueTypes["mutation_root"]>(
mutation: Q,
options?: MutationHookOptions<MapType<mutation_root, Q>, Record<string, any>>,
) {
return useMutation<MapType<mutation_root, Q>>(
gql(Zeus.mutation(mutation)),
options,
)
}
export function useTypedSubscription<Q extends ValueTypes["subscription_root"]>(
subscription: Q,
options?: SubscriptionHookOptions<
MapType<subscription_root, Q>,
Record<string, any>
>,
) {
return useSubscription<MapType<subscription_root, Q>>(
gql(Zeus.subscription(subscription)),
options,
)
}
export function useTypedClientQuery<Q extends ValueTypes["query_root"]>(
apollo: ApolloClient<NormalizedCacheObject>,
query: Q,
options?: QueryOptions<MapType<query_root, Q>, Record<string, any>>,
) {
return apollo.query<MapType<query_root, Q>>({
query: gql(Zeus.query(query)),
...options,
})
}
export function useTypedClientMutation<Q extends ValueTypes["mutation_root"]>(
apollo: ApolloClient<NormalizedCacheObject>,
mutation: Q,
options?: MutationOptions<MapType<mutation_root, Q>, Record<string, any>>,
) {
return apollo.mutate<MapType<mutation_root, Q>>({
mutation: gql(Zeus.mutation(mutation)),
...options,
})
}
function useTypedClientSubscription<Q extends ValueTypes["subscription_root"]>(
apollo: ApolloClient<NormalizedCacheObject>,
subscription: Q,
options?: SubscriptionOptions<
MapType<subscription_root, Q>,
Record<string, any>
>,
) {
return apollo.subscribe<MapType<subscription_root, Q>>({
query: gql(Zeus.subscription(subscription)),
...options,
})
}
Step Five: Iterate on the pattern
- How to generate it
- How it works under the hood
- How to adapt it to your needs by using the underlying type definitions
- Re-run the SDK generator (in our case,
yarn generate-gql-client
) - Restart all consuming clients of the SDK (in our case, only Next.js)
- Confirm that:
-
- The TypeScript compiler passes typechecks (IE that you have not made an incompatible signature change)
-
- That all services still function normally (Typically done via test suites)
-
Tutorial 2 (using GraphQL Code Generator)
Overview
Setup
- We will need a
.yml
file containing configuration - The generation process will need to be run either continually, or each time we change the queries we use in our codebase
# Create a new package.json
yarn init -y
# Add GraphQL Codegen dependencies, and GraphQL "TypedDocumentNode" typings package
yarn add -D @graphql-codegen/cli @graphql-codegen/gql-tag-operations-preset @graphql-typed-document-node/core
# Add GraphQL itself, and URQL (used as a demonstration)
yarn add graphql urql
# Initialize Typescript
tsc --init
# Put the URL to your Hasura app here
schema: http://localhost:8080/v1/graphql
# schema: ./schema.graphql <-- Alternatively, you can use a GraphQL schema file
documents:
- 'pages/**/*.tsx'
- '!utils/gql/**/*'
generates:
./utils/gql/:
preset: gql-tag-operations-preset
- Read the schema using an introspection query from the API hosted at
http://localhost:8080/v1/graphql
- Use every file matching
./pages/**/*.tsx
as a document to check for GraphQL fragments/queries- Omit every file under
./utils/gql/**/*
-- don't check these (this is where we are going to put our generated files, we don't want to double-generate)
- Omit every file under
- For generated files:
- in the directory
./utils/gql/
- use the plugin
gql-tag-operations-preset
on the documents that were found
- use the plugin
- in the directory
Development Process
import { gql } from "../utils/gql";
import { useQuery } from "urql";
const UsersQuery = gql(/* GraphQL */ `
query UsersQuery {
user {
id
email
name
}
}
`);
yarn graphql-codegen
yarn graphql-codegen --watch
Related reading