Easiest way to create GraphQL API for a TypeScript module

Marcin HagmajerJanuary 9, 2021

GraphQL is a popular API query language that offers more structured, performant and secure ways of accessing data from other services than REST. It requires the server to define a schema that lists all types of data available for clients of the API.

When building a GraphQL server to allow access to a TypeScript module, one needs to make sure that the types in GraphQL schema reflect accurately what data is expected. Let’s take a look at the following example of a simple bookstore module.

Defining bookstore module

// bookstore.ts (part 1/3)

type int = number;

export interface Book {
  id: int;
  title: string;
  authorId: int;
};

export interface Author {
  id: int;
  name: string;
};

We use a custom type alias int here to indicate that the given properties are integers rather than using the generic JavaScript number.

Here is a simple database for our bookstore:

// bookstore.ts (part 2/3)

const booksDb: Book[] = [
  {
    id: 0,
    title: 'Romeo and Juliet',
    authorId: 0,
  },
  {
    id: 1,
    title: 'The Mysterious Affair at Styles',
    authorId: 1,
  },
  {
    id: 2,
    title: 'Endless Night',
    authorId: 1,
  },
];

const authorsDb: Author[] = [
  {
    id: 0,
    name: 'William Shakespeare',
  },
  {
    id: 1,
    name: 'Agatha Christie',
  },
];

The functions below allow reading data from the bookstore:

// bookstore.ts (part 3/3)

/** get all books */
export function getBooks() {
  return booksDb;
}

/** get all authors */
export function getAuthors() {
  return authorsDb;
}

/** get the author of the given book */
export function author(book: Book) {
  return authorsDb.find((author) => author.id === book.authorId);
}

/** get all books of the given author */
export function books(author: Author) {
  return booksDb.filter((book) => book.authorId === author.id);
}

In order to make these functions available in GraphQL, we need to define a GraphQL schema. Let’s compare a few ways of doing this.

The native way (~60 lines of code)

The GraphQL implementation in JavaScript comes with theGraphQLSchema class which can be used as follows to define the schema.

// schema.ts (the native way)

import {
  GraphQLInt,
  GraphQLList,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString,
} from 'graphql';

import { author, books, getAuthors, getBooks } from './bookstore';

const bookType = new GraphQLObjectType({
  name: 'Book',
  fields: () => ({
    id: {
      type: GraphQLInt,
    },
    title: {
      type: GraphQLString,
    },
    authorId: {
      type: GraphQLInt,
    },
    author: {
      type: authorType,
      resolve: author,
    },
  }),
});

const authorType = new GraphQLObjectType({
  name: 'Author',
  fields: () => ({
    id: {
      type: GraphQLInt,
    },
    name: {
      type: GraphQLString,
    },
    books: {
      type: new GraphQLList(bookType),
      resolve: books,
    },    
  }),
});

export const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      getBooks: {
        type: new GraphQLList(bookType),
        resolve: getBooks,
      },
      getAuthors: {
        type: new GraphQLList(authorType),
        resolve: getAuthors,
      },
    },
  }),
});

The TypeGraphQL way (~70 lines of code)

TypeGraphQL is a quite popular solution for creating GraphQL schemas integrated with TypeScript. It depends on decorators, so make sure you have experimentalDecorators turned on in your config file if you wish to use it. Here is how it works in our case.

// schema.ts (TypeGraphQL way)

import 'reflect-metadata';
import {
  buildSchemaSync,
  Field,
  FieldResolver,
  Int,
  ObjectType,
  Query,
  Resolver,
  Root
} from 'type-graphql';

import * as bookstore from './bookstore';

@ObjectType()
class Book implements bookstore.Book {
  @Field((type) => Int)
  id!: number;

  @Field((type) => String)
  title!: string;

  @Field((type) => Int)
  authorId!: number;
}

@ObjectType()
class Author implements bookstore.Author {
  @Field((type) => Int)
  id!: number;

  @Field((type) => String)
  name!: string;
}

@Resolver()
class BookstoreResovler {
  @Query((returns) => [Book])
  getBooks() {
    return bookstore.getBooks();
  }

  @Query((returns) => [Author])
  getAuthors() {
    return bookstore.getAuthors();
  }
}

@Resolver((of) => Book)
class BookResolver {
  @FieldResolver((type) => Author)
  author(@Root() book: Book) {
    return bookstore.author(book);
  }
}

@Resolver((of) => Author)
class AuthorResolver {
  @FieldResolver((type) => [Book])
  books(@Root() author: Author) {
    return bookstore.books(author);
  }
}

export const schema = buildSchemaSync({
  resolvers: [BookstoreResovler, BookResolver, AuthorResolver],
});

Previewing GraphQL schema

Having theschema object defined we can use the GraphiQL tool included in express-graphql package to preview the server:

// server.ts

import express from 'express';
import { graphqlHTTP } from 'express-graphql';

import { schema } from './schema';

const port = 4000;

express()
  .use(
    '/graphql',
    graphqlHTTP({
      schema,
      graphiql: true,
    })
  )
  .listen(port);

Here’s the result:

image of graphql playground

As you can see, to define GraphQL schema that accurately reflects the bookstore module we had to repeat all the type and resolver information in a way that GraphQL could understand. This form of code repetition is undesirable because it is tedious and error prone. For example, TypeScript won’t be able to check if the GraphQL types you provided are correct.

The easy way (~20 lines of code)

At xFAANG we like finding solutions to problems like this to empower developers to spend more time writing meaningful code. If your module is already written in TypeScript it’s possible to look into its definitions and derive from them what GraphQL types need to be used. We have created typescript-graphql package that does that automatically!

When using the package, you first need to export Query object that includes top level resolvers for read-only access or Mutation object for mutating resolvers (if you have any). You may also export additional objects that include field resolvers for your object types. Any additional values exported from the module are ignored.

// bookstore.ts (additional exports)

export const Query = {
  getAuthors,
  getBooks,
};

export const Author = {
  books,
};

export const Book = {
  author,
};

Secondly, you need to provide the absolute path to the module you wish to expose in GraphQL:

// bookstore.ts (additional exports)

export const Query = {
  getAuthors,
  getBooks,
};

export const Author = {
  books,
};

export const Book = {
  author,
};

Also, if your build phase includes compilation of TypeScript into JavaScript with tsc, you also need to run npx tsgc on any modules that you wish to expose with GraphQL.

npx tsgc bookstore.ts

This will create bookstore.graphql.json file in your outDir that includes type information necessary to generate schema at runtime. That’s it! You just saved yourself writing and maintaining a lot of GraphQL code.


At the time of writing this article, typescript-graphql is published in a proof-of-concept alpha version as an open source project licensed under MIT. We’re looking forward to hearing feedback from you both in comments and the repository! Please clap 👏 if you liked the article.

Let's chat about your project

We respect your time. To avoid back-and-forth emails to schedule a call, pick a time and date for us to connect and decide on the best steps forward to start working together.