December 14, 2020

Announcing Nexus 1.0: A Major Release for Type-Safe, Code-First GraphQL APIs

Prisma is a core contributor to Nexus, a library for building code-first and type-safe GraphQL APIs. Nexus has just reached 1.0. In this post, we'll recap what Nexus is and the value it brings and what's new at 1.0.

Nexus is a library originally authored by Tim Griesser that allows developers to build code-first and type-safe GraphQL APIs. Prisma has been a core contributor to the library for over two years and has helped shape its evolution.

Today, we're happy to announce that we've released Nexus 1.0.

This release is the culmination of outstanding community feedback and contributions, years of battle-testing Nexus in production, and responding to lessons learned in creating an excellent developer experience for those building GraphQL APIs.

Note: Prisma's products are no longer GraphQL-centric. You can use Prisma in a REST API, a GraphQL API, or in any other setting where you want to access a database in Node or Go. Although we contribute to Nexus, it is not necessary to use it alongside Prisma. Find out more about what Prisma offers and how to use it.

What is Nexus?

Nexus offers a way to build code-first GraphQL APIs in Node. The code-first approach to building a GraphQL API is in contrast to the (potentially more common) schema-first approach.

Most developers that are new to building GraphQL APIs start out by taking the schema-first approach that has been popularized by companies like Apollo and their Apollo Server offering.

With the schema-first approach, writing the GraphQL API requires a set of type definitions and accompanying resolvers. A simple schema-first server might look like this:

type Post {
id: ID!
title: String!
body: String!
}
type Query {
posts: [Post]!
}
const Query = {
posts: () => [
{
id: '1',
title: 'My first GraphQL server',
body: 'How I wrote my first GraphQL server',
},
],
}

While the schema-first approach is easy to get started with, it comes with some inherent drawbacks that can make development difficult when applications start getting bigger.

Nexus takes a different approach to building GraphQL APIs. Instead of keeping a separate schema and set of resolvers, with Nexus, schemas and resolvers are written in the same spot using code.

Refactoring the Post example above to Nexus would look like this:

import { objectType, queryType, makeSchema } from 'nexus'
const Post = objectType({
name: 'Post',
definition(t) {
t.id('id')
t.string('title')
t.string('body')
},
})
const Query = queryType({
definition(t) {
t.list.field('posts', {
resolve: () => [
{
id: '1',
title: 'My first GraphQL server',
body: 'How I wrote my first GraphQL server',
},
],
})
},
})
const schema = makeSchema({
types: [Post, Query],
})

There are numerous benefits to taking a code-first approach with Nexus, including:

  • Schema and resolver co-location
  • SDL and type generation
  • No need for extra tooling

Schema and Resolver Co-location

When building a schema-first GraphQL API, it is common to start out by placing all type definitions and resolvers in a single file. When both the schema and the resolvers live next to one another, it's fairly straightforward to work in both at the same time.

As the application grows, however, it is most often desired to move parts of the schema into their own separate modules and files. It's at this point that working on a GraphQL API becomes a bit more tedious. With this modularization comes the need to switch back and forth between the Schema Definition Language and JavaScript/TypeScript to write the resolvers. Not only does one need to constantly switch between files, they also need to do a context switch mentally to work between the two langauges.

With Nexus, our schema and its resolvers are always defined together. Nexus also allows us to write everything in a common language. This allows us to side-step the co-location/context switching issue altogether and helps us to be more productive, even as our applications grow to be quite large.

Automatic Type and Schema Definition Language Generation

One major benefit of using Nexus is its ability to automatically generate TypeScript types and GraphQL Schema Definition Language (SDL) files. The generated types are useful for adding extra type safety to the code used to power your GraphQL API. The generated SDL files can be used for many purposes. For example, we can configure our editors to know about the shape of our APIs to give us introspection for the queries and mutations we write.

Type and SDL generation comes for free with Nexus and can be enabled by supplying some configuration in the makeSchema call.

import path from 'path'
const schema = makeSchema({
types: [Post, Query],
outputs: {
schema: path.join(__dirname, 'generated/schema.gen.graphql'),
typegen: path.join(__dirname, 'generated/nexusTypes.gen.ts'),
},
})

What's New at Nexus 1.0?

There are a number of changes to Nexus at 1.0. Read the full changelog and follow along below to see what's new!

New Package Name

Nexus 1.0 is now available under the nexus package name. All imports now come from nexus and not @nexus/schema.

import { makeSchema } from 'nexus'
// ...

Changes to Nullability

In previous versions of Nexus, fields were treated as non-nullable by default. This differed from other GraphQL API frameworks which would treat fields as nullable unless otherwise specified. The Nexus authors took this approach because allowing fields to be non-nullable by default posed some long-term risks for API development.

The default nullability for fields has now been inverted in Nexus so that we can align with GraphQL best practices and expectations, specifically from the authors of GraphQL.

At Nexus 1.0, you need to explicitly make fields non-null:

const Post = objectType({
name: 'Post',
definition(t) {
t.nonNull.id('id')
t.nonNull.string('title')
t.nonNull.string('body')
},
})

The SDL for this type would look like this:

type Post {
id: ID!
title: String!
body: String!
}

The code for the Post object type above uses a property called nonNull which offers a bit more flexibility for specifying that fields should be non-null. It's useful for situations where the chaining API is not ideal, such as expressing deeply-nested types and when programmatically creating non-nullable types.

import { queryType, stringArg, nonNull } from 'nexus'
queryType({
definition(t) {
t.field('tags', {
type: nonNull('String') // => String!
args: {
id: nonNull(stringArg()) // or nonNull('String') => String!
},
resolve() {
// ...
}
})
}
})

The nonNull function accepts an argument which can be used to specify the type for a type or argument.

While fields are now nullable by default, it's possible to change this behavior either globally or at the type level in your Nexus API.

queryType({
nonNullDefaults: {
output: true,
},
definition(t) {
t.string('echo', {
args: {
message: 'String',
},
resolve(_root, args) {
return args.message
},
})
},
})
Copy

In this example, Nexus now expects that fields should be non-null for all query type fields.

If you choose to change a type to be non-null by default, you can use the nullable function to specify that certain fields should be nullable.

import { queryType, stringArg, nullable } from 'nexus'
queryType({
nonNullDefaults: {
input: true,
output: true,
},
definition(t) {
t.field('echo', {
type: nullable('String'),
args: {
message: nullable(stringArg()),
},
resolve(_root, args) {
return args.message
},
})
},
})

To find out more about how Nexus handles nullability, including other ways to interact with the API, read the Nullability guide.

Changes to the List API

Nexus 1.0 introduces a new function for working with list types. The list function can be applied to inputs and outputs similar to how the nonNull and nullable functions are.

import { queryType, stringArg, list } from 'nexus'
queryType({
definition(t) {
t.field('tags', {
type: list('String') // -> [String]
args: {
ids: list(stringArg()) // or list('String') -> [String]
},
resolve() {
// ...
}
})
}
})

The same chaining API for creating lists still remain, but the list function exist to help for situations where chaining is not ideal.

Abstract Types

In GraphQL, Abstract types refer to Union and Interface types.

Union types allow you to express polymorphic fields where members types can be totally different.

Interface types let you express polymorphic fields wherein the field may return a number of different object types but they all share some subset of fields.

The official GraphQL JavaScript package supports three strategies for implementing Abstract types. Nexus 1.0 now offers an API for implementing these three strategies, providing type safety along the way.

Note that the following examples work with union types but it works similarly for interface types as well.

Centralized Strategy (resolveType)

The Centralized strategy allows you to discriminate your union member types in a centralized (to the union type) way. For example:

const SearchResult = unionType({
name: 'SearchResult',
resolveType(data) {
const __typename = data.album ? 'Song' : data.rating ? 'Movie' : data.width ? 'Photo' : null
if (!__typename) {
throw new Error(`Could not resolve the type of data passed to union type "SearchResult"`)
}
return __typename
},
definition(t) {
t.members('Photo', 'Movie', 'Song')
},
})

Discriminant Model Field Strategy (__typename)

The Discriminant Model Field (DMF) strategy allows you to discriminate your union member types in a potentialy modular way. It is based on supplying at __typename field in the model data returned by resolvers of fields typed as an abstract type. Here is an example:

const Query = queryType({
definition(t) {
t.field('search', {
type: 'SearchResult',
args: {
pattern: stringArg(),
},
resolve(root, args, ctx) {
return ctx.db.search(args.pattern).map(result => {
const __typename = result.album ? 'Song' : result.rating ? 'Movie' : result.width ? 'Photo' : null
if (!__typename) {
throw new Error(`Could not resolve the type of data passed to union type "SearchResult"`)
}
return {
...result,
__typename,
}
})
},
})
},
})

Modular Strategy (isTypeOf)

The Modular strategy allows you to discriminate your union member types in a modular way (surprise). It uses a predicate function that you implement that allows Nexus (actually GraphQL.js under the hood) to know at runtime if data being sent to the client is of a respective type or not. Here is an example:

const Movie = objectType({
name: 'Movie',
isTypeOf(data) {
return Boolean(data.rating)
},
definition(t) {
t.string('url')
t.field('rating', {
type: 'MovieRating',
})
},
})
const Photo = objectType({
name: 'Photo',
isTypeOf(data) {
return Boolean(data.width)
},
definition(t) {
t.string('url')
t.int('width')
t.int('height')
},
})
const Song = objectType({
name: 'Song',
isTypeOf(data) {
return Boolean(data.album)
},
definition(t) {
t.string('url')
t.string('album')
},
})

Read the Abstract Types guide for details on how strategies work, are enabled or disabled, are type safe, and more.

Changes to Backing Types

As you begin to implement a schema for the first time, you will notice something that may not have been obvious at first. The data that the client sees in the data graph is not the same data flowing through the internal resolvers used to fulfill that graph. The client sees the API types but the API author deals with something else, traditionally referred to as "backing" or "root" types in Nexus.

At Nexus 1.0, "backing" and "root" types are now globally referred to as "Source Types".

Read the Source Types guide to find out more about what they are and how to work with them.

Improvements to Documenation and Guides

Nexus 1.0 offers an improved documentation experience including additional guides, better navigation, JSDoc support, and upgrades to the Nexus Playground.

Improved JSDoc Support

JSDoc support provides an improved developer experience. The content of the docs includes:

  • Clear explanations
  • Concise code examples
  • Links to the official docs
  • Links to the GraphQL spec

JSDoc in NexusJSDoc in Nexus

Codesandbox Examples

The Nexus playground has been converted to a set of prepared Codesandbox examples. The new Playground offers a ready-to-go Nexus API that you can fork and extend.

Wrapping Up

Nexus 1.0 represents a significant milestone in the library's evolution. The community has played a crucial role in the its formation and success over the past 2+ years. We'd like to thank everyone who has tried out Nexus, submitted issues and pull requests, and has put it into production!

Follow @nexusgql on Twitter and watch the repo to get future updates on what's happening with Nexus.

Join the discussion

Follow @prisma on Twitter

Don’t miss the next post!

Sign up for the Prisma newsletter