A toolkit to convert GraphQL introspection to TypeScript code and generate GraphQL query from JavaScript object.
This repository contains the following two packages:
generate-graphql-client
A command line tool to generate TypeScript code from your GraphQL introspection (via JSON file or URL). This module should be installed as dev dependency. You can click here to read the docs.generate-graphql-query
A module to help us generate GraphQL query from JavaScript object. This module has zero dependencies and the bundle size of it is small (under 3kB). This module should be installed as dependency for your web application. You can click here to read the docs.
You can click here to try GraphQL Toolkit online.
GraphQL Toolkit is a lightweight tool for GraphQL client in TypeScript. It helps us to interact with GraphQL APIs in a type-safe way.
GraphQL Toolkit has a command line tool which can generate all types (enums, unions, objects and inputs) found in your GraphQL introspection to TypeScript code (with related comments in the schema). It also generates a factory function which we can use to create a GraphQL client. The created client supports every operation (queries and mutations) your GraphQL API provides.
If we are not using GraphQL Toolkit, we may write some code like the following snippet:
type Country = {
code: string
name: string
}
const continentCodes = ['AF']
const query = `
query {
countries (
filter: {
continent: {
in: ${JSON.stringify(continentCodes)}
}
}
) {
code
name
}
}
`
sendQuery<{ countries: Country[] }>(query).then((res) => {
console.log(res.data?.countries)
})
The code will work as expected. However, it has the following disadvantages:
- We have to define the type manually.
- The query is constructed by concatenating string which can be error-prone.
- Since the query is a string, it cannot be type checked.
With the client created with the factory function generated by GraphQL Toolkit, now we can write our code like the following snippet:
client.queries
.countries({
$args: {
filter: {
continent: {
in: ['AF']
}
}
},
code: true,
name: true
})
.then((countries) => {
countries.forEach((country) => {
// The type of country is `Country`.
console.log(country.name)
})
})
The code above is type-safe because the argument and return value are typed. Because the values are typed, now our code can get autocompleted thanks to TypeScript. Plus, we don't need to build our query manually anymore. GraphQL Toolkit will generate the query based on the argument we passed in.
In the above example, we only send one query (of which the name is countries
) to the server. GraphQL allow us to send multiple queries in a single request. To do that with GraphQL Toolkit, we can use the client.query
method. For example:
client
.query({
continents: {
code: true,
name: true
},
countries: {
code: true,
name: true
}
})
.then((res) => {
if (res.data) {
console.log(res.data.continents)
console.log(res.data.countries)
}
})
The generated client has the following properties:
query
A function that can be used to send multiple queries.quires
An object containing all query methods the GraphQL API supports.mutation
A function that can be used to send multiple mutations.mutations
An object containing all mutation methods the GraphQL API supports.
Note: If the GraphQL API does not provide any queries,
query
andqueries
will not be generated. And if the GraphQL API does not provide any mutations,mutation
andmutations
will not be generated.
In the previous section, we have introduced GraphQL Toolkit. Now it's time to get started.
Firstly, we should install the command line tool we mentioned before:
npm i generate-graphql-client --save-dev
If you don't want to install the command right now, there is a GraphQL Toolkit online playground that you can paste your GraphQL introspection JSON in and then get the TypeScript output. Click here to try the online playground.
Next, we should create a config file. In our example, we will save our config file to <root>/src/graphql/config.json
, and the content of config is:
{
"files": [
{
"filename": "./countries.json",
"output": "./countries.ts"
}
]
}
The filename
is the path of the GraphQL introspection file. You can click here to learn how to query the introspection from your GraphQL endpoint.
The
countries.json
in the example config is a GraphQL introspection file we grabbed from https://countries.trevorblades.com. You can find the content ofcountries.json
here.
Now we can run the generate-graphql-client
command to generate TypeScript code for us:
npx generate-graphql-client --config src/graphql/config.json
The generated code will be saved to the <root>/src/graphql/countries.ts
as we configured in config.json
(by the way the paths in config.json
is relative to the path of config.json
).
The countries.ts
file contains all types we found in the introspection (countries.json
) and exports a function named createGraphQLClient
as its default export. We can use this function to create our GraphQL client. But before that, we should install generate-graphql-query
. This module will help us generate query from JavaScript object.
# generate-graphql-query has zero dependency and its bundle size is small
npm i generate-graphql-query --save
Now, create new a file and save it to <root>/src/graphql/client.ts
with the following content:
// We will use generate-graphql-query to generate the query.
import { generateQuery } from 'generate-graphql-query'
// Import the generated factory function.
import createGraphQLClient from './countries'
// Define a function to send GraphQL query.
const sendQuery = async (query: string) => {
// In this example we will use the Fetch API.
// You can use whatever you want, maybe axios for example.
// You can also add authorization headers here if needed.
return fetch('https://countries.trevorblades.com/', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
}).then((res) => res.json())
}
// Create the GraphQL client with the generated factory function.
// The factory function accepts a async function as its parameter.
const client = createGraphQLClient(
async (
/**
* Operation type.
*/
type: 'query' | 'mutation',
/**
* The operations name.
*
* If `name` is `null`, means that the caller is `query()` or
* `mutation()`. If `name` is a string, means that the caller
* is `queries.xxx()` or `mutations.xxx()`.
*/
name: string | null,
/**
* The request payload.
*
* If `name` is `null`, `payload` is the first parameter of
* `query()` or `mutation()`. If `name` is a string, `payload`
* is the first parameter of `queries.xxx()` or `mutations.xxx()`.
*/
payload: any,
/**
* Custom options. The second parameter of the client methods.
*/
options?: any
) => {
// The `options` parameter is designed to pass some extra options.
// But in this example, we will not use it.
// If name is `null`, means that the caller function is `query()` or
// `mutation()` and `payload` is the first parameter of `query()` or
// `mutation()`. In this case, we should return the entire response json.
if (name === null) {
return sendQuery(generateQuery({ [type]: payload }))
}
// If `name` is a string, means that the caller function is `queries.xxx()`
// or `mutations.xxx()` and `payload` is the first parameter of
// `queries.xxx()` or `mutations.xxx()`. In this case, we should return
// the expected data and throw error if something went wrong.
return sendQuery(generateQuery({ [type]: { [name]: payload } })).then(
(res) => {
if (res.errors?.[0]) {
throw new Error(res.errors[0].message)
}
return res.data[name]
}
)
}
)
export default client
Now we can use the client somewhere else:
import client from '@/graphql/client'
// Get country by the code.
client.queries
.country({
$args: { code: 'BR' },
name: true,
capital: true,
currency: true,
languages: { code: true, name: true }
})
.then((country) => {
console.log(country)
})
Below is a more complicated example. This example demonstrates the following features:
- Dynamic arguments.
- Passing Arguments to sub-fields.
- Using alias.
- Using directives.
import client from '@/graphql/client'
async function getCountry({
codes,
withContinent
}: {
codes?: string[]
withContinent?: boolean
}) {
return client.queries.countries({
// If `codes` is `undefined`, the arguments will be skipped.
$args: {
filter: {
code: { in: codes }
}
},
code: true,
name: {
// Setting alias to the `name` field.
$alias: 'name_zh',
// Passing arguments to the `name` field.
$args: { lang: 'zh' }
},
continent: {
// Using the `@include` directive.
$directives: {
name: '@include',
args: { if: withContinent }
},
code: true,
name: true
}
})
}
getCountry({ codes: ['BR'], withContinent: false }).then((countries) => {
console.log(countries)
})
Next, you can read the docs of generate-graphql-query
to get familiar with the format of the argument of generateQuery
. Especially if you want to know how to pass enumeration values to the server.