Developing GraphQL APIs Using TypeGraphQL

avatar
Yanko Valera
23 Nov 2021
  • Share

In a previous blog post, we reviewed the different approaches for building a GraphQL API: the standard, schema-first approach and a code-first approach using Nexus.

In this article we will explore another popular JavaScript code-first library: TypeGraphQL. We will use the same schema as before, the same in-memory data, and Apollo Server. This way we can easily compare how we can create the same API using different tools for building the schema.

Code-first in JavaScript with TypeGraphQL

Previously we used a simple eCommerce schema:

input BuyProductInput {
	count: Int = 1
	productId: ID!
}

type Cart {
	id: Int
	items: [CartProduct]!
}

type CartProduct {
	count: Int
	product: Product
}

type Mutation {
	buyProduct(input: BuyProductInput!): Cart!
}

type Product {
	id: ID
	inStock: Boolean
	price: Int
	title: String
}

type Query {
	products: [Product]!
}
jsx

The main idea behind TypeGraphQL is to define the schema using TypeScript classes and decorators.

If we have this simple class:

export class Product {
	id: string;
	title: string;
	price: number;
}
jsx

Using decorators, we can instruct TypeGraphQL to generate the GraphQL Product Type:

import { ObjectType, Field, ID, Root, Int } from "type-graphql";

@ObjectType()
export class Product {
	@Field((type) => ID)
	id: string;
	
	@Field()
	title: string;
	
	@Field((type) => Int)
	price: number;
}
jsx

Any fields on the Product class that use the @Field decorator will be added to the Schema, others will be ignored, so we always manage which fields are internal for the application and which ones will be added to the GraphQL Type. The @Field decorator will infer the type, but sometimes we need to manually specify it like ID and Int above.

Once we have our types ready, let's add the Queries, Mutations, and FieldResolvers by defining classes and adding decorators:

@Resolver((of) => Product)
export class ProductResolver {
	@FieldResolver()
	inStock(@Root() product: ProductEntity): boolean {
		return product.stock > 0;
	}

	@Query((returns) => [Product])
	products(@Ctx() context: Context) {
		return context.db.products;
	}
}
jsx

We can have as many resolver classes as we need and TypeGraphQL allows easy decoupling of business logic and services using Dependency Injection.

Similar to Nexus, we can extend types defined in a different module, favoring schema modularization.

//cart module
@ObjectType()
export class BaseCartProduct {
	@Field((type) => ID)
	count: number;
}

//product module
@ObjectType()
export class CartProduct extends BaseCartProduct {
	@Field((type) => Product)
	product(@Root() parent: CartProductEntity, @Ctx() ctx: Context) {
		return ctx.db.products.find(
      		(value: ProductEntity) => value.id == parent.productId
    	);
	}
}
jsx

We can also add a Mutation using the specific decorator.

@Resolver((of) => Cart)
export class CartResolver {
	@Mutation((returns) => Cart)
	async buyProduct(
		@Arg("input") input: BuyProductInput,
		@Ctx() ctx: Context
	): Promise<CartEntity> {
		return ctx.db.addToCart({
			productId: input.productId,
			count: input.count!,
			customerId: ctx.customer.id,
		});
	}

	@FieldResolver()
	items(@Root() cart: CartEntity) {
		return Array.from(cart.cartProducts.values());
	}
}
jsx

TypeORM Integration

TypeORM is one of the most mature NodeJS ORMs. Similar to TypeGraphQL it uses classes and decorators to bind our objects and our database.

import { ObjectType, Field, ID, Root, Int } from "type-graphql";
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@ObjectType()
@Entity()
export class Product {
	@Field((type) => ID)
	@PrimaryGeneratedColumn()	
	id: string;
	
	@Field()
	@Column()	
	title: string;
	
	@Field((type) => Int)
	@Column()
	price: number;
}
jsx

In a single class, we have defined a GraphQL type and an entity. Using decorators, we can generate database columns, add to GraphQL types, or just have them as simple fields if no decorator is added.

Although this can increase developer productivity and help us build apps faster it needs to be used with care, as we're exposing our database structure to the GraphQL schema. Migrating a database column could break our schema and affect client applications, so developers need to be aware of this and create a proper migration strategy for the database.

Advanced Features

Authorization

TypeGraphQL also supports authorization as a first-class feature, also by using the @Authorized decorator. Let's see an example in our new stock field:

import { ObjectType, Field, ID, Root, Int, Authorized } from "type-graphql";
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@ObjectType()
@Entity()
export class Product {
	@Field((type) => ID)
	@PrimaryGeneratedColumn()	
	id: string;
	
	@Field()
	@Column()	
	title: string;
  
	@Field((type) => Int)
	@Column()
	price: number;
	
	@Field((type) => Int)
	@Column()
	@Authorized("ADMIN") 
	stock: number;
}
jsx

If a client makes a request without an Admin role an error will be returned. Similarly, we can add authorization to queries and mutations. We can add custom auth depending on our business logic, as in the authChecker example from the TypeGraphQL repo.

Validation

For validating arguments and inputs, we can rely on the GraphQL Scalars library. But sometimes, we need more validation logic in place, and we can easily integrate class-validator which, similar to TypeGraphQL and TypeORM, relies on decorators.

import { InputType, Field, ID, Int } from "type-graphql";
import { Min, Max } from "class-validator";

@InputType()
export class BuyProductInput {
	@Field((type) => Int, { defaultValue: 1 })
	@Min(1)
	@Max(10)
	count: number;

	@Field((type) => ID)
	productId: string;
}
jsx

In this example, we are instructing TypeGraphQL to validate our count field to be between 1-10.

Final Notes

As with other code-first approaches, one downside is that it can be more difficult to understand the schema. This is especially the case for TypeGraphQL, since the schema is defined by decorated classes. To be effective, team-wide communication in the schema design process is crucial. The advantages of schema modularization, type safety, and code as a single source of truth for APIs may far outweigh the added communication overhead, especially for teams that are already comfortable using TypeScript and decorators.

For the full code repository, please visit it on GitHub.

But wait - there's more.

Nearform publishes real-world learnings on data & AI, engineering, and digital strategy - with more merged in weekly.

Insights

Perspectives on AI in engineering, product development, and strategy, for enterprise executives.

Community

Deep dives and tutorials by engineers, for engineers.

Developing GraphQL APIs using Nexus

Developing GraphQL APIs using Nexus

Yanko Valera · 16 Sept 2021 · 5 min read
Game of Types: A Song of GraphQL and TypeScript

Game of Types: A Song of GraphQL and TypeScript

Steven Musumeche · 23 May 2019 · 10 min read

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.