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.
This tutorial assumes the following:
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.
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
.
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.
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 ];
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.
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/importtype-graphql
or your resolvers)
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 }
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.