Type safety in microservices using GraphQL and TypeScript

Part I: Backend and frontend

This is part one in a two-part series of blog posts about type safety in a microservices architecture. In this part, we will look at a way to ensure type safety between the backend and frontend. In part II, we will generalize this approach to communication between any two (micro)services and discuss a novel, code-first approach to combining GraphQL schemas into a single endpoint.

Over the last couple of years, the GraphQL hype train has been steadily gaining speed, especially within the JavaScript ecosystem. So when the Cubonacci development team started out working on our new product, it was naturally decided that we should consider GraphQL as a method of communication in our microservices setup.

Monthly GraphQL downloads on NPM
Figure 1: Monthly GraphQL downloads on NPM

The GraphQL API gateway

The most common GraphQL use case today seems to be one where a GraphQL-based service is used as a gateway API. Frontend applications — such as web and mobile applications, and any other external clients — connect to the GraphQL API Gateway using the GraphQL protocol. The GraphQL API Gateway then communicates with all internal services using any protocol, most often a REST interface, RPC, or sometimes a message broker such as Kafka or RabbitMQ[1].

Using GraphQL instead of a RESTful connection between frontend and backend has a number of nice benefits. This blog post will not go into detail on this, as some great articles have been dedicated to this topic already.[2][3][4]

The GraphQL API Gateway
Figure 2: The GraphQL API Gateway

Type safety between the backend code and the GraphQL schema

Because GraphQL aims to be compatible with any programming language, including typed languages, GraphQL itself is a typed query language. The types used in GraphQL are laid out in a GraphQL schema. The GraphQL schema has support for a limited set of scalar types – integers, floating-point numbers, strings, Booleans, and identifiers –, and can be extended with objects, enumerations, arrays, and a number of other constructs.

Synchronizing the (typed) GraphQL schema with our (typed) code can be done in one of two ways:

  1. Code-first: The GraphQL schema can be generated from code
  2. Schema-first: The code can be generated from the GraphQL schema

For our product we went with a code-first approach, as this allows us to keep writing in the language that we already know and love: TypeScript.

While schema-first approaches are wide-spread, we believe that the amount of new expertise that developers are required to learn before being able to properly build scalable architecture using GraphQL’s Schema Definition Language (SDL) is a big drawback. As will become apparent later in this post and in part II of this series, we are using a novel approach to code-first GraphQL development. This new approach will enable us to gain some abilities previously only available to the schema-first paradigm.

Code-first approach: generating the GraphQL schema from code

As it turns out, there exists an amazing initiative that allows one to generate a GraphQL schema from code in TypeScript named TypeGraphQL. By adding decorators to our data types and GraphQL resolvers, the GraphQL schema can be automatically inferred. Let’s try this out in a very simple application. We will specify a single data type User, a resolver UserResolver, and an index.ts file that runs TypeGraphQL’s buildSchema command to generate a GraphQL schema (use tabs to view all code):
import { ObjectType, Field, ID } from "type-graphql";

@ObjectType()
export class User{
  @Field(type => ID)
  id!: string;

  @Field()
  fullName!: string;

  @Field(type => String, { nullable: true })
  description!: string;
}
import { Query, Arg, Resolver } from "type-graphql";
import { User } from "./User";

@Resolver()
export class UserResolver{
  @Query(type => [User])
  async getUsers(): Promise<User[]>{
    return await db.getUsers();
  }

  @Query(type => User)
  async getUserById(@Arg('id') id: string): Promise{
    const user = await db.getUserById();

    if(!user){
      throw new Error('User not found');
    }

    return user;
  }
}
import 'reflect-metadata';
import { buildSchema } from 'type-graphql';
import { UserResolver } from './UserResolver';
import { printSchema } from 'graphql';

buildSchema({
  resolvers: [ UserResolver ]
}).then(schema => {
  console.log(printSchema(schema));
});

When running our application, it will log the GraphQL schema that was created:

type Query {
  getUsers: [User!]!
  getUserById(id: String!): User!
}

type User {
  id: ID!
  fullName: String!
  description: String
}

Establishing type safety between the GraphQL schema and the frontend

Great, so now we have generated a GraphQL schema from our server-side code. Generally, the GraphQL schema is exposed on the backend side by a GraphQL server such as express-graphql or Apollo Server. The schema is then consumed on the frontend side either using plain calls to the GraphQL API, or more sophisticatedly using a framework such as Apollo Client. This communication between backend and frontend is usually, but not necessarily, done over the HTTP protocol.

We cannot wait for the types to travel along on this journey, as we are using TypeScript, which requires build-time type information rather than runtime. Fortunately, another amazing effort exists to convert types exposed in a GraphQL schema back into TypeScript types and optionally functions, called GraphQL Codegen.

Generating TypeScript types from our GraphQL schema

Let’s assume that we have set up a server on localhost:4000 using the GraphQL schema we generated previously. We now want to generate types in the frontend that correspond to the schema exposed by the backend. First, we install the required dependencies into the frontend project:

npm i graphql @graphql-codegen/cli @graphql-codegen/typescript
Then, we create a configuration file named codegen.json specifying the types we want to generate in the frontend:
{
  "schema": "http://localhost:4000/graphql",
  "generates": {
    "server-types.ts": [ "typescript" ]
  }
}

Finally, we call graphql-codegen to generate out types:

./node_modules/.bin/graphql-codegen --config codegen.json

And the types are automatically generated:

export type Maybe = T | null;

/** 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 Query = {
  __typename?: "Query";
  getUsers: Array;
  getUserById: User;
};

export type QueryGetUserByIdArgs = {
  id: Scalars["String"];
};

export type User = {
  __typename?: "User";
  id: Scalars["ID"];
  fullName: Scalars["String"];
  description?: Maybe;
};

Doing more with graphql-codegen

GraphQL is a powerful tool, with a rich and ever expanding plugin ecosystem. Generating simple types like we did in the example above is only the start of it. For example, you can use it to automatically generate React HOCs or hooks for pre-defined queries, making connecting to your backend a breeze. All of it type safe of course!

Another possibility of graphql-codegen is actually to write your backend schema in GraphQL SDL first and then generate resolvers for the schema (schema-first approach). However, like stated before, this is not our preferred approach. At some point in time, you might want to combine schemas exposed by several services into a single schema exposed by your gateway API. At this point, things become complex and the merit of the code-first approach will become more apparent.

Recap

Up until now, we have created a backend service with decorated types to automatically generate a GraphQL schema. Next, we have used the schema in our frontend application to generate TypeScript types. The connection between our backend and frontend is now completely type safe, something which would have been very hard without the excellent tooling made available over the last year.

Keep in mind, every time you change any of the types in the backend, you will need to run codegen in your frontend to keep type annotations synchronized. Even better is to make this a task of your Continuous Integration (CI) server. Whenever a new build is done, the CI server should check whether running GraphQL codegen changes the frontend types. If so, they are out of sync and calls to the backend could fail. This situation should fail the CI build.

Another option to consider is adding a pre-commit hook to check whether types are synchronized between the services. This will save developers time compared to committing their code and then finding out their build fails because they forgot to run GraphQL codegen in their frontend.

Next steps

At this point we have two services which we consider to be typesafe. Changing one in a way incompatible with the other should result in code that will not build. This is great, because that means we will find bugs earlier in the process.

Up next in part II of this series, we will see how we can build on the technology we considered in this article to make all of our microservices typesafe, and how to combine GraphQL schemas into a single endpoint using our code-first approach.

If you have any remarks, opinions, or questions following from this article, do not hesitate to get in touch!