Developing GraphQL APIs Using TypeGraphQL

November 23, 2021
Image of the exterior of a building with a honeycomb overlay

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]! }

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; }

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; }

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; } }

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 ); } }

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()); } }

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; }

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; }

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; }

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.

Related Posts

Developing GraphQL APIs using Nexus

September 16, 2021
When developing a GraphQL API there are two popular approaches to create the GraphQL Schema: the schema-first approach and the code-first. Which is better?

Game of Types: A Song of GraphQL and TypeScript

May 23, 2019
Over the last few years, the popularity of both GraphQL and TypeScript has exploded in the web ecosystem—and for good reason: They help developers solve real problems encountered when building modern web applications. One of the primary benefits of both technologies is their strongly typed nature.

Tipple: Stealing Ideas From GraphQL and Putting Them to REST

May 16, 2019
You've been using Redux for a while now. It was exciting at first, but the amount of code you need to ship a new feature is starting to creep upwards. With every new addition to the backend, you find yourself making sweeping changes across the project. Actions, reducers, containers — it feels like you're touching every file in the codebase and you ask yourself: Were things always this complicated?