TypeGraphQL and GraphQL Nexus — A Look at Code-First APIs 👀

Rohit Ravikoti

Rohit Ravikoti

COO & Co-founder @ Novvum • 8 min read

TypeGraphQL and GraphQL Nexus — A Look at Code-First APIs 👀

Ever since the Prisma team announced GraphQL Nexus, there has been much interest in code-first schema development. Here at Novvum, we think this is a massive step in the right direction, but there has been a lot of confusion over how Nexus differs from TypeGraphQL. This post aims to break down the differences and hopefully clarify the confusion.

Code-first: A programmatic API for constructing GraphQL schemas

Before we get started, let’s recap why code-first schema development came to be. A popular approach in the Node.js ecosystem for writing a GraphQL server is schema-first or SDL-first. In the schema-first convention, you first define a schema in a Schema Definition Language (SDL) and write resolvers separately. If you were using TypeScript, you would need to define the schema types a second time to be used in your code. Therefore, making a change in the schema requires changes in three different places — the SDL, the resolvers, and the TypeScript types. In code-first, the schema is defined in the code, and the SDL gets generated as an artifact. You no longer need to keep track of schema changes in multiple locations! 🎊 Which brings us to Nexus and TypeGraphQL. Both libraries aim to help with code-first schema development, but they differ significantly in how they work.

Comparing APIs of GraphQL Nexus and TypeGraphQL 🗒

Types

When building a GraphQL schema, a lot of time is spent writing types. Because of this, both Nexus and TypeGraphQL try to make this process as simple as possible.

Given the following SDL type:

type User {
  id: ID!
  fullName: String
  posts: [Posts]
}

Here is how it would be defined in TypeGraphql and GraphQL Nexus respectively:

TypeGraphQL_types.ts
import { ObjectType, Field, ID } from "type-graphql";
import Post from "./Post";

@ObjectType()
export class User {
  @Field((type) => ID)
  id: string;

  @Field({ nullable: true })
  fullName: string;

  @Field((type) => [Post])
  posts: Post[];
}
GraphQL_Nexus_types.ts
import { objectType } from "nexus"

export const User = objectType({
  name: "User"
  definition(t) {
    t.id("id", {
      description: "Id of the user"
    })
    t.string("fullName", {
      description: "Full name of the user"
    })
    t.list.field("posts", {
      type: "Post"
    })
  },
})

As you can see, the APIs of the two libraries are very different. TypeGraphQL uses classes and relies on an experimental TypeScript feature called decorators (the lines of code that start with the @ symbol). GraphQL Nexus uses native JavaScript syntax. With TypeGraphQL, the TypeScript type is simply the User class that we write. It also integrates well with other decorator-based libraries like TypeORM, sequelize-typescript or Typegoose. On the other hand, Nexus auto-generates type-definitions as we develop, and infers them in our code, giving us IDE completion and type error catching out of the box. It also has seamless integration with various databases with the upcoming Yoga 2.


Resolvers

Okay, so both libraries are very straightforward when it comes to defining types and eliminating redundant code between the schema definition and TypeScript types. Now, how would we write the resolvers? This is where the two libraries take very different approaches.

Here is how we would add resolvers for our User type in both libraries:

TypeGraphQL_resolvers.ts
import {
  ObjectType,
  Field,
  ID,
  Resolver,
  FieldResolver,
  Root,
  Ctx,
} from "type-graphql"
import Post from "./Post"
import UserInput from "./UserInput"

@ObjectType()
class User {
  @Field(type => ID)
  id: string
  @Field({ nullable: true })
  fullName: string
  @Field(type => [Post])
  posts: Post[]
}

@Resolver(of => User)
class UserResolver {
  @FieldResolver()
  posts(
    @Root()
    user: User
    @Ctx()
    ctx: Context
  ) {
    return ctx.getUser(root.id).posts()
  }
  @Query(returns => User)
  async user(
    @Arg("userId")
    userId: string
    @Ctx()
    ctx: Context
  ) {
    return ctx.getUser(userId)
  }

  @Mutation(returns => User)
  async addUser(
    @Arg("user")
    userInput: UserInput
    @Ctx()
    ctx: Context
  ) {
    const newUser = ctx.createUser(userInput)
    return newUser
  }
}
GraphQL_Nexus_resolvers.ts
import {
  objectType,
  queryField,
  mutationField,
  arg,
  idArg
} from 'nexus'

export const User = objectType({
  name: "User"
  definition(t) {
    t.id("id", {
      description: "Id of the user"
    });
    t.string("fullName", {
      description: "Full name of the user"
    });
    t.list.field("posts", {
      type: "Post"
      resolve(root, args, ctx) => (
        ctx.getUser(root.id).posts()
      )
    });
  },
});

export const user = queryField(
  "user",
  {
    type: "User"
    args: {
      userId: idArg("id of the user")
    },
    resolve: (root, args ctx) => (
      ctx.getUser(args.userId),
    )
  }
);

export const addUser = mutationField(
  "addUser",
  {
    type: "User"
    args: {
      userInput: arg({
        type: "UserInput"
        required: true
      })
    },
    resolve: (root, args ctx) => (
      ctx.createuser(args.userInput)
    ),
  }
);

With TypeGraphQL, the resolvers live separately from the type definition, similar to the SDL-first paradigm (EDIT: I stand corrected. It is possible to define resolvers in the same location as the type definition). The resolver for the posts field is defined using the @FieldResolver() decorator. You would add fields for the root Query and Mutation by using the @Query(returns => User) and @Mutation(returns => User) decorators respectively.

With GraphQL Nexus, the resolvers are part of the type definition. Another bonus is that we get code completion as we write the resolvers since Nexus is auto-generating the TypeScript types for the resolvers.

A Closer Look at the API Design Decisions 🔭

I will let the two libraries speak for themselves from their docs.

TypeGraphQL:

GraphQL Nexus:

Summary: Which One to Choose?

I hope these examples helped you understand how different the two libraries are. Let’s take a look at some of the tradeoffs of each library.

TypeGraphQL

Pros:

  • Reduces the number of places to keep track of your schema from three to two or one. TypeScript types and schema types are combined, but resolvers can either be defined separately or alongside the type definition.
  • Has been around longer, so at the time of writing, it has more features like field validation and authorization.
  • What you see is what you get when it comes to TypeScript types.

Cons:

  • Works only with TypeScript since it relies on metadata reflection.
  • The way we annotate types feels redundant when defining fields and resolvers which could lead to silent mismatch errors. More details here.

NOTE: If you are currently using TypeGraphQL and it is providing value for you or your team, please donate to them!

GraphQL Nexus

Pros:

  • Reduces the number of places to keep track of your schema from three to one. The schema and TypeScript types and resolvers are in one place.
  • Sticks to using standard JavaScript syntax and generates TypeScript types, so it works with both languages.
  • Autocompletion and type-checking support for IDEs provide a great developer experience.

Cons:

  • The younger of the two libraries, so it is missing some useful features. Some are on the horizon.
  • Since it relies on type generation, the server has to be running while developing. More info here.

Which one we prefer 🙌

At Novvum, we have been using Nexus pretty extensively, and we migrated MarvelQL, a GraphQL wrapper around the Marvel API, to use it. After also trying TypeGraphQL, we feel that Nexus has been much friendlier to work with, given us more flexibility, and enabled us to move more quickly. However, this is what works well for us, and we understand that all teams operate differently.

Let us know what you think ✍️

I hope this post proved helpful for those who were not sure of what the differences were between the two libraries. If there is still confusion, please reach out to us,or you can get a hold of me directly:

Email — rohit@novvum.io

twitter — @rovvum