Adding Typescript Types to Github's GraphQL API
Github’s GraphQL API is powerful, especially for implementing custom automation and developer tooling. Check out the Explorer to play around with some queries and mutations in their live GraphiQL environment if you’ve never used it before.
When interacting with the Github GraphQL API in Typescript, it would be nice to
leverage the power of types to understand the structure of queries, mutations, variables, and responses. Luckily, with a
little glue code between graphql-codegen
and Github’s published schema
@octokit/graphql-schema
, we can add types to all of our interactions with
the API.
If you’re looking for the TLDR, the complete sample code is available on Github.
Prerequisites
If you don’t already have a Typescript / GraphQL repository initialized, go ahead and create one.
mkdir my-typescript-project
cd my-typescript-project
npm init -y
npm install --save graphql
npm install --save-dev typescript ts-node
npx tsc --init
Generating Types from the Github GraphQL Schema
To start, we’ll generate types from the Github GraphQL schema, provided by
@octokit/graphql-schema
. We’ll need to install the schema package and
graphql-codegen
.
npm install --save-dev @octokit/graphql-schema @graphql-codegen/cli
Next, we’ll use the graphql-codegen
wizard to configure our initial
code generation config.
npx graphql-codegen init
Welcome to GraphQL Code Generator!
Answer few questions and we will setup everything for you.
? What type of application are you building? (Use arrow keys)
❯ Backend - API or server
Application built with Angular
Application built with React
Application built with Stencil
Application built with Vue
Application using graphql-request
Application built with other framework or vanilla JS
Select the type of app you’re building. For this tutorial, we’ll demo a Backend - API or server
. Highlight “Backend -
API or server”, press the space bar to select it, and press enter.
? Where is your schema?: src/generated/github-schema-loader.ts
We’ll point graphql-codegen
at the schema published by
@octokit/graphql-schema
that we installed earlier. For now, type
src/generated/github-schema-loader.ts
and press enter. We’ll create that file in a subsequent step.
? Pick plugins: (Press <space> to select, <a> to toggle all, <i> to invert selection)
◉ TypeScript (required by other typescript plugins)
◉ TypeScript Resolvers (strongly typed resolve functions)
◯ TypeScript MongoDB (typed MongoDB objects)
❯◉ TypeScript GraphQL document nodes (embedded GraphQL document)
I like to write distinct GraphQL files for improved IDE support. To support this workflow, we want to use
typescript-document-nodes
. To do so, use
the down arrow key to select “TypeScript GraphQL document nodes (embedded GraphQL document)”, press space and then press
enter.
? Where to write the output: (src/generated/graphql.ts)
In this case, the default suggestion (src/generated/graphql.ts
) is just fine. Press enter to continue.
? Do you want to generate an introspection file? (Y/n) n
For this demo, we won’t need an introspection file. Type n
and press
enter.
? How to name the config file? (codegen.ts)
Naming the graphql-codegen
configuration file codegen.ts
(the default indicated above) is fine with me. Press enter
to continue.
? What script in package.json should run the codegen? codegen
This will create an entry in the package.json
scripts
object. I like to use codegen
. Type codegen
and press
enter.
Config file generated at codegen.ts
$ npm install
To install the plugins.
$ npm run codegen
To run GraphQL Code Generator.
Great, we’re all done with the wizard. Install all the plugins the wizard wrote to package.json
’s devDependencies
.
npm install
Now, write the src/generated/github-schema-loader.ts
file we referenced earlier. Create a file at
src/generated/github-schema-loader.ts
and paste the following code:
import { schema } from "@octokit/graphql-schema";
export default schema.json;
This will load the schema up from the package published by Github,
@octokit/graphql-schema
.
Finally, you’ll need to add ts-node/register
to your codegen.ts
file so the github-schema-loader.ts
can be
transpiled. The file should look like this:
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: "src/generated/github-schema-loader.ts",
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript", "typescript-resolvers", "typescript-document-nodes"],
},
},
// Add this line
require: ["ts-node/register"],
};
export default config;
Now, run your first codegen!
npm run codegen
> my-typescript-project@1.0.0 codegen /private/tmp/my-typescript-project
> graphql-codegen --config codegen.ts
✔ Parse configuration
✔ Generate outputs
Check out src/generated/graphql.ts
. We’ve now got lots of Github-related types generated!
<trimmed>
export type Repository = Node & ProjectOwner & RegistryPackageOwner & RegistryPackageSearch & Subscribable & Starrable & UniformResourceLocatable & RepositoryInfo & {
__typename?: 'Repository';
/** A list of users that can be assigned to issues in this repository. */
assignableUsers: UserConnection;
/** A list of branch protection rules for this repository. */
branchProtectionRules: BranchProtectionRuleConnection;
/** Returns the code of conduct for this repository */
codeOfConduct?: Maybe<CodeOfConduct>;
/** A list of collaborators associated with the repository. */
collaborators?: Maybe<RepositoryCollaboratorConnection>;
/** A list of commit comments associated with the repository. */
commitComments: CommitCommentConnection;
/** Identifies the date and time when the object was created. */
createdAt: Scalars['DateTime'];
/** Identifies the primary key from the database. */
databaseId?: Maybe<Scalars['Int']>;
/** The Ref associated with the repository's default branch. */
defaultBranchRef?: Maybe<Ref>;
/** Whether or not branches are automatically deleted when merged in this repository. */
deleteBranchOnMerge: Scalars['Boolean'];
/** A list of deploy keys that are on this repository. */
<trimmed>
Checkpoint
If you ran into any problems following the steps above view Checkpoint #1 on Github.
Writing Queries and Mutations
Now that we have types generated let’s do something with them! We’ll write some simple GraphQL queries and mutations with added type safety. I like to keep my queries and mutations in their own folders, so create a directory for each:
mkdir -p src/mutations src/queries
And reference those directories in your codegen.ts
file:
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: "src/generated/github-schema-loader.ts",
// Add this line
documents: ["src/mutations/*.graphql", "src/queries/*.graphql"],
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript", "typescript-resolvers", "typescript-document-nodes"],
},
},
require: ["ts-node/register"],
};
export default config;
For the next steps, you’ll also need a Classic Github Personal Access token with repo
scope. If you don’t already have
one, create one in your settings. For help doing this, check out this
Github support page.
Create an Apollo Client
For this demo, I’ll use Apollo as my GraphQL client. Install Apollo and its dependencies:
npm install --save @apollo/client cross-fetch
Now, we’ll write some boilerplate code to generate a GraphQL client. Create src/client.ts
and paste this code:
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from "@apollo/client/core";
import fetch from "cross-fetch";
export function githubClient(): ApolloClient<NormalizedCacheObject> {
if (!process.env.GITHUB_TOKEN) {
throw new Error(
"You need to provide a Github personal access token as `GITHUB_TOKEN` env variable. See README for more info."
);
}
return new ApolloClient({
link: new HttpLink({
uri: "https://api.github.com/graphql",
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
},
fetch,
}),
cache: new InMemoryCache(),
});
}
Don’t worry about understanding this code if it’s unfamiliar. All it’s doing is setting up a GraphQL client to use when communicating with the Github GraphQL API. You can use any other network client you like.
GraphQL Codegen Typescript Operations
To add types to our queries, mutations, and variables, let’s add the
@graphql-codegen/typescript-operations
plugin
to generate Typescript classes from our .graphql
files.
npm install --save-dev @graphql-codegen/typescript-operations
and reference it in your codegen.ts
file under the plugins
list.
generates: {
"src/generated/graphql.ts": {
plugins: [
"typescript",
"typescript-resolvers",
"typescript-document-nodes",
"typescript-operations", // Add this line
],
},
},
Writing A Query
Now, let’s write a simple query. Create src/queries/who-am-i.graphql
and paste the following code:
query WhoAmI {
viewer {
login
}
}
Since we have a new GraphQL query, run the codegen step to create create the appropriate Typescript types.
npm run codegen
This viewer
query will return your username (docs). Now, this is
where the generated types come in super-handy. Create an index file at src/index.ts
and paste the following code:
import { githubClient } from "./client";
import { WhoAmIQuery, WhoAmI } from "./generated/graphql";
async function whoAmI() {
const result = await githubClient().query<WhoAmIQuery>({
query: WhoAmI,
});
return result.data.viewer.login;
}
async function main() {
const username = await whoAmI();
console.info(`Your github username is ${username}`);
}
main().catch((e) => console.error(e));
By providing the <WhoAmIQuery>
type, Typescript now understands the object that will be returned from the query. We
can also pass the query as WhoAmI
, instead of writing our GraphQL statement in-line as a string.
When we run this code, we should see our username written out to the console. To run the code, you’ll need to provide
your Github token generated above as the GITHUB_TOKEN
environment variable. In your terminal, run:
GITHUB_TOKEN=PASTE_YOUR_GITHUB_TOKEN_HERE npx ts-node src/index.ts
Your github username is blimmer
Pretty cool! But that was a lot of work, right? Things get more interesting when you add start working with variables and mutations.
Checkpoint
If you ran into any problems following the steps above view Checkpoint #2 on Github.
Writing a Mutation
Next, lets write a mutation that requires a variable to show the power of layer in types when communicating with the API.
Create src/mutations/add-star.graphql
and paste the following code:
mutation AddStar($starrableId: ID!) {
addStar(input: { starrableId: $starrableId }) {
starrable {
stargazers {
totalCount
}
}
}
}
This code will add a star on the repository ID you pass (docs).
Note that we’re using a GraphQL variable called starrableId
that will
be set at runtime. Since we’ve added a .graphql
file, we need to run codegen again.
npm run codegen
Let’s update our src/index.ts
to call this mutation.
import { githubClient } from "./client";
import { WhoAmIQuery, WhoAmI, AddStarMutation, AddStarMutationVariables, AddStar } from "./generated/graphql";
async function whoAmI() {
const result = await githubClient().query<WhoAmIQuery>({
query: WhoAmI,
});
return result.data.viewer.login;
}
// !!! New Function !!!
async function starRepo(repoId: string) {
const result = await githubClient().mutate<AddStarMutation, AddStarMutationVariables>({
mutation: AddStar,
variables: {
starrableId: repoId,
},
});
if (result.errors) {
throw new Error("Mutation failed!");
}
console.info(`The repository now has ${result.data?.addStar?.starrable?.stargazers.totalCount} stargazers!!`);
}
async function main() {
const username = await whoAmI();
console.info(`Your github username is ${username}`);
const benLimmerDotComRepoId = "MDEwOlJlcG9zaXRvcnkxMjUwOTk3OA==";
await starRepo(benLimmerDotComRepoId);
}
main().catch((e) => console.error(e));
Note that we’ve added the starRepo
method that calls our new mutation. By passing
<AddStarMutation, AddStarMutationVariables>
, Typescript will now ensure that we’re passing all the required variables
to the mutation, and that they’re of the correct type. This is great because it will help catch bugs before we even call
the Github API.
For example, if I don’t pass the required starrableId
variable, I get a handy warning in my editor.
As before, let’s run this code. Note that, if you’re using the code as-is, you’ll add a star to the
blimmer/benlimmer.com
public Github repository.
GITHUB_TOKEN=PASTE_YOUR_GITHUB_TOKEN_HERE ts-node src/index.ts
Your github username is blimmer
The repository now has 7 stargazers!!
Great! We just used the Github GraphQL API to add a star to my repository (thanks!)
Checkpoint
If you ran into any problems following the steps above view Checkpoint #3 on Github.
Combining a Query and a Mutation
The code above is great, but we hard-coded the ID of my repository, which maybe we don’t want to do.
const benLimmerDotComRepoId = "MDEwOlJlcG9zaXRvcnkxMjUwOTk3OA==";
Let’s look up the repository ID at runtime, using the repository
query, and use that for the mutation instead.
Create src/queries/get-repo-id.graphql
and paste the following code:
query GetRepoId($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
}
}
This query takes two variables (owner
and name
) and will return the repository ID of the matching repo
(docs). Remember to npm run codegen
since we created a new
.graphql
file.
Let’s update index.ts
to use the result of this query in the mutation. Paste the following code:
import { githubClient } from "./client";
import {
WhoAmIQuery,
WhoAmI,
GetRepoId,
GetRepoIdQuery,
GetRepoIdQueryVariables,
AddStarMutation,
AddStarMutationVariables,
AddStar,
} from "./generated/graphql";
async function whoAmI() {
const result = await githubClient().query<WhoAmIQuery>({
query: WhoAmI,
});
return result.data.viewer.login;
}
async function getBenLimmerDotComRepoId(): Promise<string> {
const result = await githubClient().query<GetRepoIdQuery, GetRepoIdQueryVariables>({
query: GetRepoId,
variables: {
owner: "blimmer",
name: "benlimmer.com",
},
});
if (!result.data.repository) {
throw new Error(`Couldn't find repository id!`);
}
return result.data.repository.id;
}
async function starRepo(repoId: string) {
const result = await githubClient().mutate<AddStarMutation, AddStarMutationVariables>({
mutation: AddStar,
variables: {
starrableId: repoId,
},
});
if (result.errors) {
throw new Error("Mutation failed!");
}
console.info(`The repository now has ${result.data?.addStar?.starrable?.stargazers.totalCount} stargazers!!`);
}
async function main() {
const username = await whoAmI();
console.info(`Your github username is ${username}`);
const benLimmerDotComRepoId = await getBenLimmerDotComRepoId();
await starRepo(benLimmerDotComRepoId);
}
main().catch((e) => console.error(e));
Note that we’re now calling getBenLimmerDotComRepoId()
instead of hard-coding the value. Typescript is also helping us
make sure we have a null
check, in case the repository can’t be found. That information from Typescript reminded us to
add this block of code to ensure the repository could be found from the API call:
if (!result.data.repository) {
throw new Error(`Couldn't find repository id!`);
}
Let’s run the code:
GITHUB_TOKEN=PASTE_YOUR_GITHUB_TOKEN_HERE ts-node src/index.ts
Your github username is blimmer
The repository now has 7 stargazers!!
The output is the same, but we’re now dynamically querying for the repository ID before we call the AddStar
mutation.
Checkpoint
If you ran into any problems following the steps above view Checkpoint #4 on Github.
Conclusion
As part of this tutorial, we created a project to interact with the
Github GraphQL API. By using
graphql-codegen
, we were able to add Typescript types to make interacting with
the API easier and safer.
Because @octokit/graphql-schema
is
automatically updated any time Github GraphQL schema is
updated, we can simply update the package and ensure our Typescript still compiles.
npm update @octokit/graphql-schema
Without generating types from the schema, Github could change their schema in a way that breaks our code, and we wouldn’t know until runtime.
I hope this walkthrough was helpful and helps your next integration with Github a little bit more fun!