Developing GraphQL APIs using Nexus

16 September 2021

Image by  Paolo Chiabrando

When developing a GraphQL API there are two popular approaches to create the GraphQL Schema: the schema-first approach and the code-first. The schema-first consists of building the Schema using the Schema Definition Language while the code-first uses a programming language to create the Schema.

In this blog post, we will explore both alternatives, outline the benefits of the code-first approach, and see it in action with GraphQL Nexus.

Let's see with a code example what it means to define our schema with schema-first and code-first.

Schema defined using the Schema Definition Language (SDL):

# Profile Type
type Profile {
  id: ID!
  username: String!
}

# The "Query" type defines all queries users can execute
type Query {
  me: Profile!
}

The same schema is defined using a Programming Language, in this case, Javascript with Nexus library:

import { objectType, scalarType } from '@nexus/schema';

const Profile = objectType({
  name: "Profile",
  definition(t) {
    t.nonNull.id("id")
    t.nonNull.string("username")
  }
})

const Query = objectType({
  name: "Query",
  definition(t) {
    t.nonNull.field("me", {
      type: Profile,
      resolve() {
        return {
          id: 1, 
          username: "blogpost"
        };
      }
    })
  }
});

The Benefits of a Code-first Approach

The schema-first has great benefits: it uses a common language that's easy to understand by all team members and improves the API design and communication between backend and front-end teams. However, once we start adding more and more types to our Schema, we might have a few issues with this approach:

  • If a type changes, the resolver for the matching type needs to be updated. Keeping all types and resolvers in sync becomes a challenge in larger schemas.
  • We need additional tools to modularize the schema as it's very hard to have all the schema types in a single file.
  • It's harder to reuse types, causing duplications.

The code-first, on the other hand, can be harder to understand by team members—but given that we're building the schema using code, it will solve the schema-first issues outlined above.

  • We can easily modularize the schema as it just means modularizing our code.
  • The code is the single source of truth, and the types are generated from the code.
  • We can reuse types and do anything that the underlying programming language supports when building the schema.

Code-first in JavaScript

Depending on the programming language used there are different alternatives to building code-first GraphQL APIs. In JavaScript, two popular libraries are Nexus and TypeGraphQL. In the next section, we'll explore Nexus, and in a future blog post, we might go deep with TypeGraphQL.

GraphQL Nexus

GraphQL Nexus is a declarative, code-first library for building GraphQL Schemas. GraphQL Nexus can work with any GraphQL server like Apollo, or middleware like Mercurius.

The API is simple but provides us all the features we can build with graphql-js. Let's see it in action, with a basic e-commerce GraphQL API.

We will use Apollo Server, an in-memory database and the goal is to create a modular schema from the beginning.

First, let's imagine how we might solve the problem with a schema-first approach:

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

Now let's see how we can build this schema using Nexus, in a modular, reusable way. Let's add two different files. Each file will have all related types, queries, and mutations: Product.ts and Cart.ts

//Product.ts
import { extendType, objectType } from "nexus";

//inStock is the only field we need a custom resolver.
export const Product = objectType({
  name: "Product",
  definition(t) {
    t.id("id");
    t.string("title");
    t.int("price");
    t.boolean("inStock", {
      resolve(parent) {
        return parent.stock > 0;
      },
    });
  },
});

//add products Query
export const ProductQuery = extendType({
  type: "Query",
  definition(t) {
    t.nonNull.list.field("products", {
      type: "Product",
      resolve(_root, _args, ctx) {
        return ctx.db.products;
      },
    });
  },
});

We can see in the Product type that id/title/price fields do not need a resolver function. For inStock, we need a resolver to calculate this value depending on the stock number. Colocating the boolean field definition and the resolver makes it easier to reason about and easier to update.

In Cart.ts we will define all the types related to the Cart:

//Cart.ts
import { extendType, inputObjectType, objectType, nonNull } from "nexus";

export const Cart = objectType({
  name: "Cart",
  definition(t) {
    t.int("id");
    t.nonNull.list.field("items", {
      type: "CartProduct",
      resolve(parent, _args) {
        return Array.from(parent.cartProducts.values());
      },
    });
  },
});

export const BuyProductInput = inputObjectType({
  name: "BuyProductInput",
  definition(t) {
    t.nonNull.id("productId");
    t.int("count", { default: 1 });
  },
});

export const CartMutation = extendType({
  type: "Mutation",
  definition(t) {
    t.nonNull.field("buyProduct", {
      type: "Cart",
      args: {
        input: nonNull(BuyProductInput),
      },
      resolve(_root, { input }, ctx) {
        return ctx.db.addToCart({
          productId: input.productId,
          count: input.count!,
          customerId: ctx.customer.id,
        });
      },
    });
  },
});

export const BaseCartProduct = objectType({
  name: "CartProduct",
  definition(t) {
    t.int("count");
  },
});

One interesting feature is extending the BaseCartProduct type in Product.ts and adding the product field. This way we keep all related features in Product.ts and each module is responsible for a feature or type.

//Product.ts

/* .... types defined above */

//this type will extend CartProduct defined in Cart.ts and add the product: Product field
export const CartProductWithProduct = extendType({
  type: "CartProduct",
  definition(t) {
    t.field("product", {
      type: "Product",
      resolve(parent, _args, ctx) {
        return ctx.db.products.find(
          (product) => product.id == parent.productId
        );
      },
    });
  },
});

Resolvers, Types, and Generated Artifact

As we create our types using code, the definition and resolvers of the types are colocated in the same place. This facilitates the process of updating our types and resolvers.

Nexus, by default, generates a schema file and the TypeScript types matching our GraphQL Schema. It's also a good practice to commit the generated files to the repository. Type-safety is a big plus and Nexus is almost 100% type-safe by default.

Nexus Plugin System

When we need to reuse functionality or to define our own abstractions, we can create or use a Nexus Plugin. Nexus Plugin API can be used to define new options for types and fields and modify our schema.

There are some plugins ready to use, like the Relay Connection plugin, which allows us to easily enable paginated associations following the Relay Specification. The full list can be found here.

Transitioning to Nexus

One downside of code-first approaches and Nexus is difficulty in understanding between different teams compared to the Schema Definition Language. This is a major downside, but there are possible solutions to this problem.

One solution is to use the Schema Definition Language to design our API Schema, easing the communication between teams, and use a tool like SDL converter to generate the initial Nexus code and then use GraphQL Nexus to develop the API.

Final Notes

In this post, we have discussed different alternatives to develop a GraphQL API, and how code-first approaches can help us with defining a modular GraphQL Schema, with the code being the single source of truth, and with an improved developer experience and type-safety compared to traditional schema-first approaches.

In future posts, we will explore other tools available in the JavaScript landscape to develop code-first GraphQL APIs so stay tuned!

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

Related Posts

Urql, Grown Up

Early 2018 we released the first version of our minimalist GraphQL client `urql`. For the last year, we’ve been rethinking, rearchitecting, and rebuil ...

Phil Plückthun

Phil Plückthun

Read More

Check out more of Yanko's blog posts