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

Rohit Ravikoti

Rohit Ravikoti

April 16 • 5 Min Read

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:

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[] }
TypeGraphQL 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", }) }, })
GraphQL Nexus types.ts

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.


UPDATE: Here is a blog post outlining how to integrate GraphQL Nexus with Prisma.

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:

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 } }
TypeGraphQL 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) ), } );
GraphQL Nexus resolvers.ts

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:

To add a new field to our entity, we have to jump through all the files: modify the entity class, then modify the schema, and finally update the interface. The same goes with inputs or arguments: it’s easy to forget to update one of them or make a mistake with a type. Also, what if we’ve made a typo in a field name? The rename feature (F2) won’t work correctly.

TypeGraphQL comes to address these issues, based on experience from over a year of developing GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using classes and a bit of decorator help. Additional features like dependency injection, validation and auth guards help with common tasks that would normally have to be handled by ourselves.

GraphQL Nexus:

The core idea of GraphQL Nexus draws from basing the schema off the SDL — keeping things declarative and simple to understand. It allows you to reference the type names as string literals rather than always needing to import to reference types (you can do that too if you prefer).

By combining automatic type generation with some of the more powerful features of TypeScript — type merging, conditional types, and type inference, we can know exactly which type names we are referring to and able to use throughout our code. We can know both the parameters and the return type of resolvers without providing any type annotation. It takes a little getting used to, but it ends up leading to a great feedback loop of the types annotating themselves.

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


Tags
GraphQLAuthenticationTutorialWeb DevelopmentTypeScriptJavaScript