Getting started / Tutorial

2. Writing your first schema

Overview

In this chapter you're going to write your first schema. You'll learn about:

  • Writing GraphQL objects
  • Exposing GraphQL objects for query operations
  • GraphQL SDL file generation
  • Enhanced type safety & autocompletion

Reflection?

Before we get going we need a moment to introduce an important part of the Nexus development workflow. Nexus has an unconventional concept called "Reflection". It refers to the fact that, when makeSchema is being called, not only is your application code being run, but information is being gathered and artifacts are being derived. Some of Nexus' uses for reflection include:

  • Generating TypeScript types to give your resolvers complete type safety
  • Generating an SDL file

This partly explains why Nexus has a declarative API. It needs a way to run your app reliably at build time. Declarative APIs give Nexus a higher degree of control to do this. Declarative APIs also encode enough semantic value for Nexus to do the things it needs to.

It also explains the need for the --transpile-only flag passed to ts-node-dev. If you don't know what it is about, it basically tells TypeScript not to typecheck your app. Since nexus needs to be run in order to generate TypeScript types, we need to ensure that nexus won't ever be prevented from generating these types because of a type error.

You might be (rightfully) wondering: "Wait, how am I supposed to benefit from Nexus' type-safety if I disable it in ts-node-dev... ?". For now, the answer to that is to use your IDE's type-checker. If your IDE/terminal doesn't have one, then manually run an additional tsc process.

Just remember the following though. You should always have your npm run dev script running when you're working on your project even when you're not intending to use your server (e.g. access the GraphQL Playground). If you forget to run npm run dev then you will not, for example, get the static typing experience that you expect in your resolvers.

Model the domain

Let's get started with our blog schema by modeling some key entities in the domain. We'll begin with the concept of a Post. A post will represent a page in your blog. When not published, we'll call it a draft.

Your modeling work is going to start on the API layer as opposed to the database layer. This API-first approach can be a good way to collaborate with frontend teams, getting their input in shaping the data early.

A note about terminology. We will be talking about the Post Object, not the Post Model. The difference is that at the API layer we have objects but at the database layer we have models. The name difference helps us talk about these different layers without confusion. It is also how GraphQL (API layer) and Prisma (database layer, discussed later) respectively refer to these things.

Create a new module for your Post object at api/graphql/Post.ts. We could write our whole schema within say api/schema.ts or api/graphql.ts, but modularizing your GraphQL type definitions can help scale your codebase. Neither approach is inherently wrong though, so do as you see you fit. For this tutorial we'll use the modular style.

$mkdir api/graphql && touch api/graphql/Post.ts

To create the Post object we'll import the objectType function from the nexus package. This will help you building a GraphQL Object Types.

1// api/graphql/Post.ts
2
3import { objectType } from 'nexus'
4
5export const Post = objectType({
6 name: 'Post', // <- Name of your type
7 definition(t) {
8 t.int('id') // <- Field named `id` of type `Int`
9 t.string('title') // <- Field named `title` of type `String`
10 t.string('body') // <- Field named `body` of type `String`
11 t.boolean('published') // <- Field named `published` of type `Boolean`
12 },
13})

Additionally, we need to pass that Post object type down to our makeSchema function. To achieve that, we'll create an api/graphql/index.ts file, which will be used as an index to re-export all types from your schema.

1// api/graphql/index.ts
2
3export * from './Post'

Finally, we'll import this file and pass it down to makeSchema

Diff
Code
1// api/schema.ts
2import { makeSchema } from 'nexus'
3import { join } from 'path'
+ import * as types from './graphql'
5
6const schema = makeSchema({
- types: []
+ types,
9 outputs: {
10 typegen: join(__dirname, '../nexus-typegen.ts'),
11 schema: join(__dirname, '../schema.graphql')
12 }
13})

Note: It is considered best practice to pass your types directly from a "star import" like we've done above. Under the hood, Nexus will unwrap the types. This prevents from constantly having to manually export & import every single type of your API.

SDL?

Once you've saved this file change to disk, your app will be restarted.

You may notice that there's also now a new schema.graphql file at your project root. It contains a representation of your schema in a syntax called the GraphQL Schema Definition Language (SDL for short). In dev mode Nexus generates this for you at every app restart. In it you should see the following:

1type Post {
2 body: String
3 id: Int
4 published: Boolean
5 title: String
6}

You are free to disable this file (settings discussed later) but its existence has two benefits for you to consider:

  1. For users familiar with SDL the correspondence between the source code and it may help them learn Nexus' schema API faster.
  2. The SDL syntax makes it an accessible way for others to evaluate incoming API changes without having to know about Nexus, or even JavaScript. Consider using the generated SDL file to improve your pull-request reviews.

For the remainder of this tutorial we'll be keeping SDL to the right of Nexus code blocks.

Your first home-grown query

Your Post object is in place now but there's still no way for clients to read that data. Let's change that. You'll use the special Query object to expose your Post object.

We'll start by letting API clients read the drafts of your blog.

NOTE: This should cause a static type error when you save the file. Don't worry, we'll fix it in a second!

1// api/graphql/Post.ts // 1
2
3import { extendType } from 'nexus'
4
5export const PostQuery = extendType({
6 type: 'Query', // 2
7 definition(t) {
8 t.nonNull.list.field('drafts', { // 3, 4, 5
9 type: 'Post', // 6, 7
10 })
11 },
12})
1type Query {
2 drafts: [Post]!
3}
  1. The Query object is a central place in your schema where many other types will appear. Like before with the modular GraphQL types decision we again can decide to be modular here. We could either create a new api/graphql/Query.ts module (not modular), or we could collocate the exposure of Post object with its definition in api/graphql/Post.ts (modular). Staying consistent with before, we'll take the modular way.

  2. To achieve collocation in Nexus we'll use schema.extendType. Its API is very similar to schema.objectType with the difference that the defined fields are merged into the targeted type.

  3. .nonNull specifies that clients will always get a value for this field. By default, in Nexus, all "output types" (types returned by fields) are nullable. This is for best practice reasons. In this case though we indeed want to guarantee that a list will always be returned, never null. If you're ever dissatisfied with Nexus' defaults, not to worry, you can change them.

  4. .list augments the field's type spec, making it wrapped by a List type. Here, a [Post].

  5. The first parameter specifies the field's name, here drafts

  6. type: 'Post' specifies what the field's type should be. Here, a Post

  7. Nexus also allows you to specifiy lists and nullability on the type field. This example could be rewritten like so 👇

    1t.field('drafts', {
    2 type: nonNull(list('Post')),
    3})

You'll see some feedback from your IDE that you're missing a resolve property. Go ahead and try to implement it, letting the autocompletion guide you.

You might be wondering why Nexus hasn't complained about missing resolvers in some other cases so far. The answer is a more advanced topic that we'll cover later.

Diff
Code
1// api/graphql/Post.ts
2import { extendType } from 'nexus'
3// ...
4
5export const PostQuery = extendType({
6 type: 'Query',
7 definition(t) {
8 t.nonNull.list.field('drafts', {
9 type: 'Post',
+ resolve() {
+ return [{ id: 1, title: 'Nexus', body: '...', published: false }]
+ },
13 })
14 },
15})

Try it out

You can now open up your GraphQL playground and try the following query (left); In response, you should see something like so (right):

1{
2 drafts {
3 id
4 title
5 body
6 published
7 }
8}
1{
2 "data": {
3 "drafts": [
4 {
5 "id": 1,
6 "title": "Nexus",
7 "body": "...",
8 "published": false
9 }
10 ]
11 }
12}

Wrapping up

Congratulations! You've successfully got your first GraphQL schema up and running with Nexus! In the next chapter we'll explore adding some write capabilities to our API.

Next →
Edit this page on Github