How to Build a GraphQL API with TypeGraphQL and TypeORM


This article was originally written for LogRocket.

GraphQL’s popularity is constantly growing, and it’s no mystery as to why: it’s a great tool that solves many common problems developers encounter with RESTful APIs. GraphQL allows us to easily fetch data relations, but it also prevents us from overfetching that data. Put simply, GraphQL improves the development experience and makes frontend apps faster.

Despite its many advantages, however, building a GraphQL API can occasionally present challenges. How can we minimize the headaches we encounter in our projects?

First, we need to have a schema that defines our types, mutations, and queries with SDL. Then, we need to write the resolvers that will resolve the values for our schema. We also need to define our ORM models that represent the data in our database. The fields we defined in our models need to conform to our schema or it won’t work.

The main issue with this approach is that it’s difficult to maintain. If we want to change a field in our data, we need to change the database model class and GraphQL schema and adjust the type interface (if using TypeScript). But in this tutorial, I’m going to show you an enjoyable way to build a GraphQL API with TypeGraphQL and TypeORM.

TypeGraphQL is a framework for building GraphQL APIs with Node.js and TypeScript. The main purpose of this tool is to let us define our schema directly from our TypeScript code. TypeORM, on the other hand, is a TypeScript library that allows us to interact with SQL databases. With these tools combined, we can build a type-safe GraphQL API without the frustrations that usually come with such a project.

In this tutorial, we’ll demonstrate how to build a GraphQL API with TypeGraphQL and TypeORM that can manage books data with CRUD functionalities.

Prerequisites

Before you get started, make sure that you:

  1. Understand JavaScript
  2. Have a general understanding of Node.js and NPM
  3. Have basic knowledge of TypeScript

Getting started

We’ll begin by initializing a new Node.js project.

mkdir learn-typegraphql
npm init -y

Next, we’ll install some dependencies.

npm install apollo-server type-graphql typeorm reflect-metadata

Here we are installing:

In addition, we need to install some development dependencies.

npm install -D typescript ts-node nodemon

This script will install:

  1. TypeScript to compile our code to plain JavaScript
  2. ts-node to run our server in development environment
  3. nodemon to automatically restart the server whenever we make changes to the code

Now, to make our job a bit easier, let’s define the NPM start scripts in package.json.

package.json
{
	// ...
	"scripts": {
		"start": "nodemon -w src --ext ts --exec ts-node src/index.ts"
	}
}

Next, create a tsconfig.json file. This file contains our TypeScript configurations, since we will use some TypeScript features that are currently still experimental, yet stable enough for our purposes.

tsconfig.json
{
	"compilerOptions": {
		"target": "es5",
		"module": "commonjs",
		"strict": true,
		"esModuleInterop": true,
		"experimentalDecorators": true,
		"emitDecoratorMetadata": true,
		"strictPropertyInitialization": false
	}
}

Make sure the experimentalDecorators and emitDecoratorMetadata are set to true.

For reference, I published the entire source code of this project to my GitHub. Feel free to poke around or clone it onto your computer.

git clone https://github.com/rahmanfadhil/learn-typegraphql.git

Setting up a GraphQL server

It’s time to start working on our server API. Let’s create a new file called index.ts inside the src folder.

src/index.ts
import "reflect-metadata"
import { createConnection } from "typeorm"
import { ApolloServer } from "apollo-server"

async function main() {
	const connection = await createConnection()
	const schema = await buildSchema()
	const server = new ApolloServer({ schema })
	await server.listen(4000)
	console.log("Server has started!")
}

In this file, we can write a function called start. This function makes it easier to initialize every single library that we use in this project. In this function, we’ll first create a new connection to our database with the createConnection function provided by TypeORM.

Next, we’ll generate our GraphQL schema with the buildSchema method by TypeGraphQL. This will take all our resolvers and generate an executable GraphQL schema we can use inside our Apollo Server. These resolvers are a little bit different, which we’ll discuss it later in this tutorial.

The reflect-metadata package we imported at the top is a helper library that extends the functionality of TypeScript decorators. This package is required to use TypeORM and TypeGraphQL.

Finally, we’ll initialize our Apollo Server, pass our schema, and start it in port 4000 (or any other port you want).

Database configuration

Remember when we created a database connection with TypeORM? Before we do anything else, we need to define a database configuration to tell TypeORM which kind of database we plan to use and how to access it. There are several ways to do this; personally, I like to create the configuration inside the ormconfig.json file.

Currently, TypeORM supports nine types of SQL databases, including popular ones such as MySQL and PostgreSQL. You can use any database you want, but for the sake of simplicity, I’m going to use SQLite — the smallest implementation of SQL database engine that is very easy to get started. To use this database, we must first install the driver for Node.js.

npm install sqlite3

Now, we can add the ormconfig.json file into our project.

ormconfig.json
{
	"type": "sqlite",
	"database": "./db.sqlite3",
	"entities": ["./src/models/*.ts"],
	"synchronize": true
}

Resolvers

To build our GraphQL resolvers, we’ll first define the mutations, queries, and other object types in our schema with GraphQL schema language. Then, we’ll define the resolvers in our JavaScript code to resolve the values of our schema.

The resolvers are usually a collection of functions that are mapped into a single object, and it has to match with the schema we defined earlier. This approach seems very complicated because we need to define both the schema and the resolvers in a separate place.

With TypeGraphQL, however, we don’t need to explicitly write the schema. Instead, we define our resolvers with TypeScript classes and decorators, and TypeGraphQL will generate the schema for us.

We can use the code below to define our resolvers with TypeGraphQL.

src/resolvers/BookResolver.ts
import { Resolver, Query } from "type-graphql"

@Resolver()
export class BookResolver {
	@Query(() => String)
	hello() {
		return "world"
	}
}

Here we created a class called BookResolver and decorated it with the Resolver decorator by TypeGraphQL. This enables us to place all our resolvers in this class as a method. We also want to make sure we decorate the method with either Query or Mutation and pass the return type on the first parameter.

So far, we just have a hello query that will return a string. Later, we will implement full CRUD to our database with TypeORM.

Now we need to register our resolver in src/index.ts.

src/index.ts
import "reflect-metadata"
import { createConnection } from "typeorm"
import { ApolloServer } from "apollo-server"
import { BookResolver } from "./resolvers/BookResolver.ts" // add this

async function main() {
	const connection = await createConnection()
	const schema = await buildSchema({
		resolvers: [BookResolver], // add this
	})
	const server = new ApolloServer({ schema })
	await server.listen(4000)
	console.log("Server has started!")
}

That’s it! To make sure everything is set up properly, let’s try to run our server by running npm start on the terminal and opening localhost:4000 in the browser.

hello world query

Models

Now that our server is up and running, the next step is to define our models.

A model is essentially a class that allows us to interact with a specific table in our database. With TypeORM, we can define our database models with classes and decorators, just like our resolvers. And because we’re trying to build a bookstore API, let’s create a model that represents our books.

src/models/Book.ts
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class Book extends BaseEntity {
	@PrimaryGeneratedColumn()
	id: string

	@Column()
	title: string

	@Column()
	author: string

	@Column({ default: false })
	isPublished: boolean
}

A TypeORM model is essentially a plain TypeScript class that is decorated with Entity. This class contains properties that represent the fields of our table in the database. You can read more about it in the TypeORM official documentation.

This class extends the BaseEntity class, which contains useful methods to access our books table.

Object Types

Since we are building a GraphQL API, we also need to define our object types. In GraphQL, every query and mutation returns an object, whether it’s a boolean, string, or a custom object we define ourselves. Just like our models, we can simply define our object types by using classes and decorators.

Here is where the magic happens. We can combine both TypeGraphQL and TypeORM decorators in a single TypeScript class. In that way, we can have a class that represents both GraphQL object type, as well as the database model. The code should look something like this:

src/models/Book.ts
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from "typeorm"
import { ObjectType, Field, ID } from "type-graphql"

@Entity()
@ObjectType()
export class Book extends BaseEntity {
	@Field(() => ID)
	@PrimaryGeneratedColumn()
	id: string

	@Field(() => String)
	@Column()
	title: string

	@Field(() => String)
	@Column()
	author: string

	@Field(() => Boolean)
	@Column({ default: false })
	isPublished: boolean
}

This makes our code a lot more efficient because we define a single data type in one place, which should help reduce errors caused by property inconsistency.

Let’s say we want to update the isPublished property to published. Traditionally, when using the default GraphQL schema language, we’d need to define our data type in both the database model and the GraphQL schema. However, by using these decorators, we can simply update the property in our class to update both the schema and the model.

Database CRUD

After we create our database models, let’s go back to our resolvers and implement a query that returns all our books.

src/resolvers/BookResolver.ts
import { Resolver, Query } from "type-graphql"
import { Book } from "../models/Book"

@Resolver()
class BookResolver {
	@Query(() => [Book])
	books() {
		return Book.find()
	}
}

We’ll create the books method inside our resolver class and decorate it with Query. To specify the return type of our query, we need to pass it inside the Query decorator argument, which, in this case, is an array of books. Inside this method, we fetch our book with the find method from our model.

Now let’s go back to our playground and test this query.

all books query

It returns an empty array, which means we’ve yet to create any books. Let’s do so by creating a mutation.

@Mutation(() => Book)
async createBook(@Arg("data") data: CreateBookInput) {
  const book = Book.create(data);
  await book.save();
  return book;
}

Here, we’re creating a createBook method that will return a book type. In this method, we initialize a new instance of Book, save it to the database with the save method, and return it. This method requires data as a parameter. To obtain data from users, we can build an input type to specify what fields are necessary for this mutation.

Let’s create an input to create a new book.

src/inputs/CreateBookInput.ts
import { InputType, Field } from "type-graphql"

@InputType()
export class CreateBookInput {
	@Field()
	title: string

	@Field()
	author: string
}

The input type class is similar to our object type. The only difference is that we decorated the class with InputType. Also, the only fields that are necessary for creating a book are title and author, because the id is auto-generated by the database and isPublished field has a default value.

Let’s test it out!

create book query

Next, we’ll create a new query to fetch individual book.

@Query(() => Book)
book(@Arg("id") id: string) {
  return Book.findOne({ where: { id } });
}

Let’s try this query:

find one book query

So far, so good!

Now it’s time to add the update operation.

@Mutation(() => Book)
async updateBook(@Arg("id") id: string, @Arg("data") data: UpdateBookInput) {
  const book = await Book.findOne({ where: { id } });
  if (!book) throw new Error("Book not found!");
  Object.assign(book, data);
  await book.save();
  return book;
}

In the updateBook method, we need the id of the book we want to update as well as the user input, which we will create later. First, we’ll fine find the book, if it exists. Then, we’ll update the properties defined in the data parameter. Finally, we’ll save all changes to the database and return the updated book data to the user.

Below we define the input for updating a book.

src/inputs/UpdateBookInput.ts
import { InputType, Field } from "type-graphql"

@InputType()
export class UpdateBookInput {
	@Field({ nullable: true })
	title?: string

	@Field({ nullable: true })
	author?: string

	@Field({ nullable: true })
	isPublished?: boolean
}

The input is very similar to our CreateBookInput class. However, all these properties are optional, which means that user doesn’t have to fill all the properties of the book.

update book query

The last step is to implement the delete Book feature.

@Mutation(() => Boolean)
async deleteBook(@Arg("id") id: string) {
  const book = await Book.findOne({ where: { id } });
  if (!book) throw new Error("Book not found!");
  await book.remove();
  return true;
}

The method is pretty straightforward. We find that book from the given id, remove it from the database with remove function, and return true for the result.

delete book query

Conclusion

TypeGraphQL can help solve many of the issues developers encounter when building GraphQL APIs, especially with TypeScript. Not only does it offer a cleaner and safer way to build GraphQL APIs, but it also prevents us from repeating the same tasks over and over. It’s even more useful if you use TypeORM because it takes the same approach to defining your database models. These tools are proven to get the job done and should be a top consideration for your next big project.

Tags: GraphQL, Typescript, Api