Getting started with GraphQL and TypeScript

Introduction

TypeScript is a superset of JavaScript, and its adoption has skyrocketed in recent years, as many apps are now being rewritten in it. If you have ever created a GraphQL server with TypeScript, then you would know it’s not as straightforward as in the JavaScript counterpart. So in this tutorial, I'll be showing you how to use TypeScript with GraphQL using TypeGraphQL. For the purpose of demonstration, we'll be rebuilding the GraphQL server that was built in the Getting up and running with GraphQL tutorial, which is a simple task manager.

Prerequisites

This tutorial assumes the following:

  • Node.js and NPM installed on your computer
  • Basic knowledge of GraphQL
  • Basic knowledge of TypeScript
  • TypeScript installed on your computer, which you can get from the official website

What’s TypeGraphQL?

TypeGraphQL is a framework building GraphQL APIs in Node.js. It makes use of TypeScript classes and decorators for defining GraphQL schema and types as well as resolvers. With TypeGraphQL, we don’t need to manually define types in SDL or create interfaces for our GraphQL schema. TypeGraphQL allows us to have only one source of truth, that way reducing field type mismatches, typos etc.

Another interesting thing about TypeGraphQL is how well it integrates with decorator-based libraries, like TypeORM, sequelize-typescript or Typegoose. This allows us to define both the GraphQL type and the entity in a single class, so we don’t need to edit multiple files to add or rename some properties.

Getting started

To get started with TypeGraphQL, we need to first install it along with its dependencies. We’ll start by creating a new project:

1$ mkdir graphql-typescript
2    $ cd graphql-typescript
3    $ npm init -y

Then we install TypeGraphQL:

    $ npm install type-graphql

Next, we need to install TypeScript as a dev-dependency as well as types for Node.js:

    $ npm install typescript @types/node --save-dev

TypeGraphQL requires the reflect-metadata shim, so we need to install that as well:

    $ npm install reflect-metadata

Next, we need to define some TypeScript configurations for our project. Create a tsconfig.json file within the project’s root directory, and paste the snippet below into it:

1// tsconfig.json
2    
3    {
4      "compilerOptions": {
5        "target": "es2016",
6        "module": "commonjs",
7        "lib": ["dom", "es2016", "esnext.asynciterable"],
8        "moduleResolution": "node",
9        "outDir": "./dist",
10        "strict": true,
11        "strictPropertyInitialization": false,
12        "sourceMap": true,
13        "emitDecoratorMetadata": true,
14        "experimentalDecorators": true
15      },
16      "include": ["./src/**/*"]
17    }

If you have ever worked with TypeScript before (which this tutorial assumes), then you should be familiar with some of the settings above. Since TypeGraphQL makes extensive use of decorators, which are an experimental feature of TypeScript, we need to set both emitDecoratorMetadata and experimentalDecorators as true. Also, we need to add esnext.asynciterable to the list of library files, since graphql-subscription uses AsyncIterator.

Defining the GraphQL schema

We can start defining the schema for our GraphQL server. Create a new src directory, then within it, create a new schemas directory. Inside the schemas directory, create a Project.ts file and add the following code in it:

1// src/schemas/Project.ts
2    
3    import { Field, Int, ObjectType } from "type-graphql";
4    import Task from "./Task";
5    
6    @ObjectType()
7    export default class Project {
8      @Field(type => Int)
9      id: number;
10      
11      @Field()
12      name: string;
13      
14      @Field(type => [Task])
15      tasks: Task[];
16    }

We define a Project class and use the @ObjectType() decorator to define it as a GraphQL type. The Project type has three fields: id, name and tasks. We use the @Field decorator to define these fields. The @Field decorator can also accept optional arguments. We can pass to it the type the field should be or an object containing other options we want for the field. We explicitly set the type of the id field to be Int while tasks is an array of the type Task (which we’ll create shortly).

Next, let’s define the schema for the Task type. Inside the schemas directory, create a Task.ts file and add the following code in it:

1// src/schemas/Task.ts
2    
3    import { Field, Int, ObjectType } from "type-graphql";
4    import Project from "./Project";
5    
6    @ObjectType()
7    export default class Task {
8      @Field(type => Int)
9      id: number;
10    
11      @Field()
12      title: string;
13    
14      @Field(type => Project)
15      project: Project;
16    
17      @Field()
18      completed: boolean;
19    }

This is pretty similar to the Project schema. With our schema defined, we can move on to creating the resolvers.

Adding sample data

Before we get to the resolvers, let’s quickly define some sample data we’ll be using to test out our GraphQL server. Create a data.ts file directly inside the src directory, and paste the snippet below into it:

1// src/data.ts
2    
3    export interface ProjectData {
4      id: number;
5      name: string;
6    }
7    
8    export interface TaskData {
9      id: number;
10      title: string;
11      completed: boolean;
12      project_id: number;
13    }
14    
15    export const projects: ProjectData[] = [
16      { id: 1, name: "Learn React Native" },
17      { id: 2, name: "Workout" },
18    ];
19    
20    export const tasks: TaskData[] = [
21      { id: 1, title: "Install Node", completed: true, project_id: 1 },
22      { id: 2, title: "Install React Native CLI:", completed: false, project_id: 1},
23      { id: 3, title: "Install Xcode", completed: false, project_id: 1 },
24      { id: 4, title: "Morning Jog", completed: true, project_id: 2 },
25      { id: 5, title: "Visit the gym", completed: false, project_id: 2 },
26    ];

Creating the resolvers

Create a new resolvers directory inside the src directory. Inside the resolvers directory, create a ProjectResolver.ts file and paste the code below in it:

1// src/resolvers/ProjectResolver.ts
2    
3    import { Arg, FieldResolver, Query, Resolver, Root } from "type-graphql";
4    import { projects, tasks, ProjectData } from "../data";
5    import Project from "../schemas/Project";
6    
7    @Resolver(of => Project)
8    export default class {
9      @Query(returns => Project, { nullable: true })
10      projectByName(@Arg("name") name: string): ProjectData | undefined {
11        return projects.find(project => project.name === name);
12      }
13      
14      @FieldResolver()
15      tasks(@Root() projectData: ProjectData) {
16        return tasks.filter(task => {
17          return task.project_id === projectData.id;
18        });
19      }
20    }

We use the @Resolver() decorator to define the class as a resolver, then pass to the decorator that we want it to be of the Project type. Then we create our first query, which is projectByName, using the @Query() decorator. The @Query decorator accepts two arguments: the return type of the query and an object containing other options which we want for the query. In our case, we want the query to return a Project and it can return null as well. The projectByName query accepts a single argument (name of the project), which we can get using the @Arg decorator. Then we use find() on the projects array to find a project by its name and simply return it.

Since the Project type has a tasks field, which is a custom field, we need to tell GraphQL how to resolve the field. We can do that using the @FieldResolver() decorator. We are getting the object that contains the result returned from the root or parent field (which will be the project in this case) using the @Root() decorator.

In the same vein, let’s create the resolvers for the Task type. Inside the resolvers directory, create a TaskResolver.ts file and paste the code below in it:

1// src/resolvers/TaskResolver.ts
2    
3    import { Arg, FieldResolver, Mutation, Query, Resolver, Root } from "type-graphql";
4    import { projects, tasks, TaskData } from "../data";
5    import Task from "../schemas/Task";
6    
7    @Resolver(of => Task)
8    export default class {
9      @Query(returns => [Task])
10      fetchTasks(): TaskData[] {
11        return tasks;
12      }
13      
14      @Query(returns => Task, { nullable: true })
15      getTask(@Arg("id") id: number): TaskData | undefined {
16        return tasks.find(task => task.id === id);
17      }
18      
19      @Mutation(returns => Task)
20      markAsCompleted(@Arg("taskId") taskId: number): TaskData {
21        const task = tasks.find(task => {
22          return task.id === taskId;
23        });
24        if (!task) {
25          throw new Error(`Couldn't find the task with id ${taskId}`);
26        }
27        if (task.completed === true) {
28          throw new Error(`Task with id ${taskId} is already completed`);
29        }
30        task.completed = true;
31        return task;
32      }
33      
34      @FieldResolver()
35      project(@Root() taskData: TaskData) {
36        return projects.find(project => {
37          return project.id === taskData.project_id;
38        });
39      }
40    }

We define two queries: fetchTasks and getTask. The fetchTasks simply returns an array of all the tasks that have been created. The getTask query is pretty similar to the projectByName query. Then we define a mutation for marking a task as completed, using the @Mutation. This mutation will also return a Task. Firstly, we get the task that matches the supplied taskId. If we can’t find a match, we simply throw an appropriate error. If the task has already been marked as completed, again, we throw an appropriate error. Otherwise, we set the task completed value to true and lastly return the task.

Just as we did with the Project type, we define how we want to resolve the project field.

Building the GraphQL server

With everything in place, all that is left now is to tie them together by building a GraphQL server. We will be using graphql-yoga for building our GraphQL server. First, let’s install it:

    $ npm install graphql-yoga

With that installed, create an index.ts file directly inside the src directory, and paste the code below in it:

1// src/index.ts
2    
3    import { GraphQLServer } from "graphql-yoga";
4    import "reflect-metadata";
5    import { buildSchema } from "type-graphql";
6    import ProjectResolver from "./resolvers/ProjectResolver";
7    import TaskResolver from "./resolvers/TaskResolver";
8    
9    async function bootstrap() {
10      const schema = await buildSchema({
11        resolvers: [ProjectResolver, TaskResolver],
12        emitSchemaFile: true,
13      });
14      
15      const server = new GraphQLServer({
16        schema,
17      });
18      
19      server.start(() => console.log("Server is running on http://localhost:4000"));
20    }
21    
22    bootstrap();

Since we need to build our schema first before making use of it in our GraphQL server, we create an async function, which we call bootstrap() (you can name it however you like). Using the buildSchema() from type-graphql, we pass to it our resolvers and we set emitSchemaFile to true (more on this shortly). Once the schema has been built, we instantiate a new GraphQL server and pass to it the schema. Then we start the server. Lastly, we call bootstrap().

Sometimes, we might need to see or inspect the schema in SDL (Schema Definition Language) that TypeGraphQL will generate for us. One way we can achieve that is setting emitSchemaFile to true at the point of building the schema. This will generate a schema.gql file directly in project’s root directory. Of course, we can customize the path however we want.

Note: make sure to import reflect-metadata on top of your entry file (before you use/import type-graphql or your resolvers)

Testing it out

Before we start testing our GraphQL, we need to first compile our TypeScript files to JavaScript. For that, we’ll be using the TypeScript compiler. Running the command below directly from the project’s root directory:

    $ tsc

The compiled JavaScript files will be inside the dist directory, as specified in tsconfig.json. Now we can start the GraphQL server:

    $ node ./dist/index.js

The server should be running on http://localhost:4000, and we can test it out with the following query:

1# fetch all tasks
2    
3    {
4      fetchTasks {
5        title
6        project {
7          name
8        }
9      }
10    }
graphql-typescript-demo

Conclusion

In this tutorial, we looked at what is TypeGraphQL and it makes it easy to work with GraphQL and TypeScript. To learn more about TypeGraphQL and other advanced features it provides, as well as the GitHub repo.

The complete code for this tutorial is available on GitHub.