DEV Community

Cover image for Automatically generate Typescript types for your GraphQL schema
Xavier Canchal
Xavier Canchal

Posted on • Updated on

Automatically generate Typescript types for your GraphQL schema

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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}`);
})();
Enter fullscreen mode Exit fullscreen mode

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[] = [];
Enter fullscreen mode Exit fullscreen mode

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!
  }
`
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
    },
  },
// [...]
};
Enter fullscreen mode Exit fullscreen mode

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!
  }
`
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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.

Apollo Studio

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
Enter fullscreen mode Exit fullscreen mode

Finally, add a new script in the package.json for convenience:

"scripts": {
  "generate-gql-types": "graphql-codegen"
}
Enter fullscreen mode Exit fullscreen mode

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']>;
};
Enter fullscreen mode Exit fullscreen mode

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[] =[];
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

 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:

Image description

Top comments (1)

Collapse
 
sixman9 profile image
Richard Joseph

Home, déu-n’hi-do!

(I used to live in Poble Nou ;-) )