Introduction
In this post, I will show you how to automatically generate types for your GraphQL APIs written in Typescript using GraphQL codegen.
Prerequisites
Some GraphQL and Typescript knowledge is expected. NodeJS and Typescript must be installed on your machine.
Context
Typescript
Typescript is a static type checker for JavaScript. It is used as a development tool and helps writing better code and catching potential errors while developing instead that on runtime.
GraphQL
GraphQL is a query language for writing HTTP APIs. It is very flexible and can help optimizing network load as well as the number of endpoints that you would need in a typical REST API.
Apollo GraphQL
Apollo GraphQL is a framework/toolset for building GraphQL APIs. It provides solutions both for server and client.
GraphQL code generator (graphql-codegen)
graphql-codegen is a tool that automatically generates Typescript types from GraphQL types and resolvers definition.
What are we going to build
We will build a simple GraphQL API that will manage painters and it's paintings. We'll use Apollo server and graphql-codegen for generating the Typescript types automatically, which will be available to use across the codebase.
If you feel lost at any point or simply want to speed up things, here you can find the final code: https://github.com/xcanchal/apollo-server-typescript
Hands-on
First of all, create a new folder for the project and initialize the npm project:
$ mkdir {project-name}
$ cd {project-name}
$ npm init --yes
Install the following dependencies and devDependencies:
$ npm install --save apollo-server graphql
$ npm install --save-dev typescript @tsconfig/recommended graphql-codegen @graphql-codegen/cli @graphql-codegen/typescript nodemon ts-node
Create the tsconfig.json, the configuration file for Typescript . We'll use the recommended example but we'll add an extra property outDir
, because we want the generated files to be put all inside the 'dist/' folder instead of next to each original .ts
file:
{
"compilerOptions": {
"outDir": "dist",
"target": "ES2015",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Recommended"
}
To finish with the basic initial setup, add the following dev
command under the package.json
scripts. This command will be used for running the server in development mode (building the JS files and restarting it on every change):
"scripts": {
"dev": "nodemon --exec ts-node ./server.ts --watch"
}
Now, let's write the code for our GraphQL server. Create a new server.ts
file and ignore editor errors, if any, for now:
import { ApolloServer } from 'apollo-server';
import typeDefs from './type-defs';
import resolvers from './resolvers';
(async () => {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await server.listen();
console.log(`🚀 Server ready at ${url}`);
})();
We will use a couple of arrays to work as a database. Create a new file named database.ts
and paste the following content. Temporarily, we will use any
for the entities types (don't judge me, we'll fix that later!)
export const painters: any[] = [];
export const paintings: any[] = [];
Great! so now we can start defining the schema for our API. Create a new file named type-defs.ts
and add the types for the Painter
and Painting
entities:
import { gql } from 'apollo-server';
export default gql`
type Painter {
name: String!
country: String!
techniques: [String]!
}
type Painting {
author: String!
title: String!
technique: String!
date: String!
}
`
We need a way to insert new painters and paintings into our database. Let's define our first mutation in the type-defs.ts
file, which will allow us to create painters:
# [...]
input PainterInput {
name: String!
country: String!
techniques: [String]!
}
type Mutation {
createPainter(input: PainterInput!): Painter!
}
After that, let's add a similar mutation for creating paintings:
# [...]
input PaintingInput {
author: String!
title: String!
technique: String!
date: String!
}
type Mutation {
# [...]
createPainting(input: PaintingInput!): Painting!
}
The next step will be creating the resolvers, which will tell GraphQL how to query or mutate the data associated with the previously defined types.
Create a file named resolvers.ts
with the following content (again, we'll use any
until we generate the typescript types):
import { painters, paintings } from './database';
const resolvers = {
Mutation: {
createPainter(_: any, { input: painter }: any): any {
painters.push(painter);
return painter;
},
createPainting(_: any, { input: painting }: any): any {
paintings.push(painting);
return painting;
}
}
};
export default resolvers;
Up to this point, we can insert painters and paintings. The next step is to implement a few queries to retrieve the data from the database. Add the following queries to the type-defs.ts
file.
# [...]
type Query {
painters: [Painter]! #Â get all painters
paintings: [Painting]! # get all paintings
painter(name: String): Painter # get a painter by name
painting(title: String): Painting #Â get a painting by title
}
And also add the respective resolvers to the resolvers.ts
file.
// [...]
const resolvers = {
// [...]
Query: {
painters: (): any => painters,
paintings: (): any => paintings,
painter(_: any, { name }: any): any {
console.log(name);
return painters.find((painter) => painter.name === name);
},
painting(_: any, { title }: any): any {
return paintings.find((painting) => painting.title === title);
},
},
// [...]
};
Your type-defs.ts
file should look like this:
import { gql } from 'apollo-server';
export default gql`
type Painter {
name: String!
country: String!
techniques: [String]!
}
type Painting {
author: String!
title: String!
technique: String!
date: String!
}
input PainterInput {
name: String!
country: String!
techniques: [String]!
}
input PaintingInput {
author: String!
title: String!
technique: String!
date: String!
}
type Query {
painters: [Painter]!
paintings: [Painting]!
painter(name: String): Painter
painting(title: String): Painting
}
type Mutation {
createPainter(input: PainterInput!): Painter!
createPainting(input: PaintingInput!): Painting!
}
`
And the resolvers.ts
file should look like:
import { painters, paintings } from './database';
const resolvers = {
Query: {
painters: (): any => painters,
paintings: (): any => paintings,
painter(_: any, { name }: any): any {
console.log(name);
return painters.find((painter) => painter.name === name);
},
painting(_: any, { title }: any): any {
return paintings.find((painting) => painting.title === title);
},
},
},
Mutation: {
createPainter(_: any, { input: painter }: any): any {
painters.push(painter);
return painter;
},
createPainting(_: any, { input: painting }: any): any {
paintings.push(painting);
return painting;
}
}
};
export default resolvers;
Now that we have defined defining the types and resolvers for our API, let's run the server in development mode and see how it looks inside Apollo Studio, which is a playground for testing it.
Execute npm run dev
, open a new browser navigate to it:
$ npm run dev
// -> 🚀 Server ready at http://localhost:4000/
After clicking on the "Query your server" button you'll land at the Apollo Studio, where you'll be able to explore the schema definition as well as trying to execute the mutations and queries that we have implemented.
The last thing to do, and the cherry on top of this article, is to generate the Types to be used in our typescript files that match our GraphQL schema.
Let's return to the codebase to configure graphql-codegen
. Create a new file named codegen.yaml
and paste the following basic configuration (see the complete list of available options here):
schema: "./type-defs.ts" #Â GraphQL types (input file)
generates:
./gql-types.d.ts: # Typescript types (output generated file)
plugins: # List of needed plugins (installed as devDeps)
- typescript
Finally, add a new script in the package.json
for convenience:
"scripts": {
"generate-gql-types": "graphql-codegen"
}
Execute it (npm run generate-gql-types
) and see how a new file with the name we defined in the codegen.yaml
(gql-types.d.ts
) gets generated. This file contains all the Typescript types:
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};
export type Mutation = {
__typename?: 'Mutation';
createPainter: Painter;
createPainting: Painting;
};
export type MutationCreatePainterArgs = {
input: PainterInput;
};
export type MutationCreatePaintingArgs = {
input: PaintingInput;
};
export type Painter = {
__typename?: 'Painter';
country: Scalars['String'];
name: Scalars['String'];
techniques: Array<Maybe<Scalars['String']>>;
};
export type PainterInput = {
country: Scalars['String'];
name: Scalars['String'];
techniques: Array<Maybe<Scalars['String']>>;
};
export type Painting = {
__typename?: 'Painting';
author: Scalars['String'];
date: Scalars['String'];
technique: Scalars['String'];
title: Scalars['String'];
};
export type PaintingInput = {
author: Scalars['String'];
date: Scalars['String'];
technique: Scalars['String'];
title: Scalars['String'];
};
export type Query = {
__typename?: 'Query';
painter?: Maybe<Painter>;
painters: Array<Maybe<Painter>>;
painting?: Maybe<Painting>;
paintings: Array<Maybe<Painting>>;
};
export type QueryPainterArgs = {
name?: Maybe<Scalars['String']>;
};
export type QueryPaintingArgs = {
title?: Maybe<Scalars['String']>;
};
Pretty cool, right? Then you'll love it when you see how they looks when we actually use them in the code and we really benefit from the type checking:
First of all, let's update the database.ts
file:
import { Painter, Painting } from './gql-types';
export const painters: Painter[] = [];
export const paintings: Painting[] =[];
After that, do the same in the resolvers.ts
file:
import { painters, paintings } from './database';
import {
Painter,
Painting,
MutationCreatePainterArgs,
MutationCreatePaintingArgs,
QueryPainterArgs,
QueryPaintingArgs,
} from './gql-types';
const resolvers = {
Query: {
painters: (): Painter[] => painters,
paintings: (): Painting[] => paintings,
painter(: any, { name }: QueryPainterArgs): Painter | undefined {
console.log(name);
return painters.find((painter) => painter.name === name);
},
painting(: any, { title }: QueryPaintingArgs): Painting | undefined {
return paintings.find((painting) => painting.title === title);
},
},
Mutation: {
createPainter(: any, { input: painter }: MutationCreatePainterArgs): Painter {
painters.push(painter);
return painter;
},
createPainting(: any, { input: painting }: MutationCreatePaintingArgs): Painting {
paintings.push(painting);
return painting;
}
}
};
export default resolvers;
 Conclusion
Awesome! you have completed this tutorial. By following this approach, there's no need to define the same entities twice (one for GraphQL and one for Typescript) and we can focus on what really matters when designing, implementing and maintaining a GraphQL API: its schema types, queries, and mutations.
With graphql-codegen
, we get the Typescript types automatically generated and our code is consistent with the GraphQL schema without much effort, apart from any configuration tweaks that may be needed as the application evolves.
This is one of many ways to work with Typescript and GraphQL, so if you follow a different approach, don't doubt to share it so we can learn something new!
Follow me on Twitter for more content @xcanchal
Buy me a coffee:
Top comments (1)
Home, déu-n’hi-do!
(I used to live in Poble Nou ;-) )