Overview

In this chapter you're going to add some write capability to your API. You'll learn about:

  • Writing GraphQL mutations
  • Exposing GraphQL objects for mutation operations
  • Working with GraphQL Context
  • Working with GraphQL arguments

To keep our learning gradual we'll stick to in-memory data for now but rest assured a proper databases is coming in an upcoming chapter.

Wire up the context

The first thing we'll do is setup an in-memory database and expose it to our resolvers using the GraphQL context.

The GraphQL Context is a plain JavaScript object shared across all resolvers. Nexus creates a new one for each request and adds a few of its own properties. Largely though, what it will contains will be defined by your app. It is a good place to, for example, attach information about the logged-in user.

So go ahead and create the database.

$touch api/db.ts
1// api/db.ts
2
3export const db = {
4 posts: [{ id: 1, title: 'Nexus', body: '...', published: false }],
5}

Now to expose it in our GraphQL context we'll use a new schema method called addToContext. We can do this anywhere in our app but a fine place is the api/app.ts module we already created in chapter 1.

1// api/app.ts
2
3import { schema } from 'nexus'
4import { db } from './db'
5
6schema.addToContext(() => {
7 return {
8 db,
9 }
10})

That's it. Behind the scenes Nexus will use the TypeScript compiler API to extract our return type here and propagate it to the parts of our app where the context is accessible. And if ever this process does not work for you for some reason you can use fallback to manually giving the types to Nexus like so:

1declare global {
2 interface NexusContext {
3 // type information here
4 }
5}

Note For those familiar with GraphQL, you might be grimacing that we’re attaching static things to the context, instead of using export/import. This is a matter of convenience. Feel free to take a purer approach in your apps if you want.

Use the context

Now let's use this data to re-implement the Query.drafts resolver from the previous chapter.

1schema.queryType({
2 definition(t) {
3 t.list.field('drafts', {
4 type: 'Post',
- resolve() {
- return [{ id: 1, title: 'Nexus', body: '...', published: false }]
+ resolve(_root, _args, ctx) { // 1
+ return ctx.db.posts.filter(p => p.published === false) // 2
9 },
10 })
11 },
12})
  1. Context is the third parameter, usually identified as ctx
  2. Return the filtered data by un-published posts, aka drafts . Nexus makes sure the types line up.

Did you notice? Still no TypeScript type annotations required from you yet everything is still totally type safe. Prove it to yourself by hovering over the ctx.db.posts property and witness the correct type information it gives you. This is the type propagation we just mentioned in action. 🙌

Your first mutation

Alright, now that we know how to wire things into our context, let's implement our first mutation. We're going to make it possible for your API clients to create new drafts.

This mutation will need a name. Rather than simply call it createPost we'll use language from our domain. In this case createDraft seems reasonable. There are similarities with our previous work with Query.drafts:

  • Mutation is a root type, its fields are entrypoints.
  • We can colocate mutation fields with the objects they relate to or centralize all mutation fields.

As before we will take the collocation approach.

1// api/graphql/Post.ts
2// ...
3
4schema.extendType({
5 type: 'Mutation',
6 definition(t) {
7 t.field('createDraft', {
8 type: 'Post',
9 nullable: false,
10 resolve(_root, args, ctx) {
11 ctx.db.posts.push(/*...*/)
12 return // ...
13 },
14 })
15 },
16})
1Mutation {
2 createDraft: Post!
3}

We need to get the client's input data to complete our resolver. This brings us to a new concept, GraphQL arguments. Every field in GraphQL may accept them. Effectively you can think of each field in GraphQL like a function, accepting some input, doing something, and returning an output. Most of the time "doing something" is a matter of some read-like operation but with Mutation fields the "doing something" usually entails a process with side-effects (e.g. writing to the database).

Let's revise our implementation with GraphQL arguments.

1schema.extendType({
2 type: 'Mutation',
3 definition(t) {
4 t.field('createDraft', {
5 type: 'Post',
+ args: { // 1
+ title: schema.stringArg({ required: true }), // 2
+ body: schema.stringArg({ required: true }), // 2
+ },
10 resolve(_root, args, ctx) {
+ const draft = {
+ id: ctx.db.posts.length + 1,
+ title: args.title, // 3
+ body: args.body, // 3
+ published: false,
+ }
+ ctx.db.posts.push(draft)
+ return draft
- ctx.db.posts.push(/*...*/)
- return // ...
21 },
22 })
23 },
24})
1Mutation {
- createDraft: Post
+ createDraft(title: String!, body: String!): Post
4}
  1. Add an args property to the field definition to define its args. Keys are arg names and values are type specifications.
  2. Use the Nexus helpers for defining an arg type. There is one such helper for every GraphQL scalar such as schema.intArg and schema.booleanArg. If you want to reference a type like some InputObject then use schema.arg({ type: "..." }).
  3. In our resolver, access the args we specified above and pass them through to our custom logic. If you hover over the args parameter you'll see that Nexus has properly typed them including the fact that they cannot be undefined.

Model the domain: Part 2

Before we wrap this chapter let's flesh out our schema a bit more. We'll add a publish mutation to transform a draft into an actual published post, then we'll let API clients read the published posts.

1// api/graphql/Post.ts
2
3import { schema } from 'nexus'
4
5schema.extendType({
6 type: 'Mutation',
7 definition(t) {
8 // ...
9 t.field('publish', {
10 type: 'Post',
11 args: {
12 draftId: schema.intArg({ required: true }),
13 },
14 resolve(_root, args, ctx) {
15 let draftToPublish = ctx.db.posts.find(p => p.id === args.draftId)
16
17 if (!draftToPublish) {
18 throw new Error('Could not find draft with id ' + args.draftId)
19 }
20
21 draftToPublish.published = true
22
23 return draftToPublish
24 },
25 })
26 },
27})
1type Mutation {
2 createDraft(body: String!, title: String!): Post
+ publish(draftId: Int!): Post
4}
1// api/graphql/Post.ts
2
3import { schema } from 'nexus'
4
5schema.extendType({
6 type: 'Query',
7 definition(t) {
8 // ...
9 t.list.field('posts', {
10 type: 'Post',
11 resolve(_root, _args, ctx) {
12 return ctx.db.posts.filter(p => p.published === true)
13 },
14 })
15 },
16})
1type Query {
2 drafts: [Post!]
+ posts: [Post!]
4}

Try it out

Great, now head on over to the GraphQL Playground and run this query (left). If everything went well, you should see a response like this (right):

1mutation {
2 publish(draftId: 1) {
3 id
4 title
5 body
6 published
7 }
8}
1{
2 "data": {
3 "publish": {
4 "id": 1,
5 "title": "Nexus",
6 "body": "...",
7 "published": true
8 }
9 }
10}

Now, that published draft should be visible via the posts Query. Run this query (left) and expect the following response (right):

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

Wrapping up

Congratulations! You can now read and write to your API.

But, so far you've been validating your work by manually interacting with the Playground. That may be reasonable at first (depending on your relationship to TDD) but it will not scale. At some point you are going to want automated testing. Nexus takes testing seriously and in the next chapter we'll show you how. See you there!

Next →
Edit this page on Github