Getting started / Tutorial

3. Adding mutations to your API

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. Your GraphQL server creates a new one for each request. It is a good place to, for example, attach information about the logged-in user.

So go ahead and create the database with its type definition.

$touch api/db.ts
1// api/db.ts
2
3export interface Post {
4 id: number
5 title: string
6 body: string
7 published: boolean
8}
9
10export interface Db {
11 posts: Post[]
12}
13
14export const db: Db = {
15 posts: [{ id: 1, title: 'Nexus', body: '...', published: false }],
16}

Now to expose it in our GraphQL context there is two things we need to do:

  1. Pass the db object to our GraphQL server context
  2. Let Nexus know what the type of our context is

We'll begin by creating a new module to hold out the context and its type.

$touch api/context.ts
1// api/context.ts
2import { Db, db } from './db'
3
4export interface Context {
5 db: Db
6}
7
8export const context = {
9 db
10}

Then we'll pass our in-memory database to our GraphQL server

Diff
Code
1// api/server.ts
2import { ApolloServer } from 'apollo-server'
+import { context } from './context'
4import { schema } from './schema'
5
6export const server = new ApolloServer({
7 schema,
+ context
9})

The context passed to Apollo Server can either be a function or a plain JavaScript object. For this tutorial, we've used an Object for simplicity. We would need to use a function when we need to perform some calculation or get some data from an external source based on the request and then pass that derived data on to the context for all our resolvers to use.

For example, if a user is logged in to your application, it would be useful to have the information regarding the user available to all the resolvers and this information would have to be retrieved from your database or an external service. The code responsible for that database or service call would be placed in the function which is passed to Apollo Server as the context.

Finally, let's configure Nexus to know the type of our GraphQL context by adjusting the configuration of the makeSchema in our api/schema.ts.

Diff
Code
1// api/schema.ts
2import { makeSchema } from 'nexus'
3import { join } from 'path'
4import * as types from './graphql'
5
6export const schema = makeSchema({
7 types,
8 outputs: {
9 typegen: join(__dirname, '..', 'nexus-typegen.ts'),
10 schema: join(__dirname, '..', 'schema.graphql')
11 },
+ contextType: { // 1
+ module: join(__dirname, "./context.ts"), // 2
+ export: "Context", // 3
+ },
16})
  1. Option to set the context type
  2. Path to the module where the context type is exported
  3. Name of the export in that module

Use the context

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

Diff
Code
1// api/graphql/Post.ts
2
3export const PostQuery = extendType({
4 type: 'Query',
5 definition(t) {
6 t.list.field('drafts', {
7 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
12 },
13 })
14 },
15})
  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.

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
3export const PostMutation = extendType({
4 type: 'Mutation',
5 definition(t) {
6 t.nonNull.field('createDraft', {
7 type: 'Post',
8 resolve(_root, args, ctx) {
9 ctx.db.posts.push(/*...*/)
10 return // ...
11 },
12 })
13 },
14})
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.

Diff
Code
SDL
1import { objectType, extendType } from 'nexus'
+import { objectType, extendType, stringArg, nonNull } from 'nexus'
3
4export const PostMutation = extendType({
5 type: 'Mutation',
6 definition(t) {
7 t.nonNull.field('createDraft', {
8 type: 'Post',
+ args: { // 1
+ title: nonNull(stringArg()), // 2
+ body: nonNull(stringArg()), // 2
+ },
13 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 // ...
24 },
25 })
26 },
27})
  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 intArg and booleanArg. If you want to reference a type like some InputObject then use arg({ type: "..." }). You can use the helpers nonNull and nullable to adjust the nullability type of the arg. You can use the functional helper list to turn the arg into a list type too.
  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 flush out our schema a bit more. We'll add a publish mutation to transform a draft into an actual published post.

Diff
Content
SDL
1// api/graphql/Post.ts
-import { objectType, extendType, stringArg, nonNull } from 'nexus'
+import { objectType, extendType, stringArg, nonNull, intArg } from 'nexus'
4
5export const PostMutation = extendType({
6 type: 'Mutation',
7 definition(t) {
8 // ...
+ t.field('publish', {
+ type: 'Post',
+ args: {
+ draftId: nonNull(intArg()),
+ },
+ resolve(_root, args, ctx) {
+ let draftToPublish = ctx.db.posts.find(p => p.id === args.draftId)
+
+ if (!draftToPublish) {
+ throw new Error('Could not find draft with id ' + args.draftId)
+ }
+
+ draftToPublish.published = true
+
+ return draftToPublish
+ },
+ })
26 },
27})

Then, we'll let API clients read these published posts.

Diff
Content
SDL
1// api/graphql/Post.ts
2import { extendType } from 'nexus'
3
4export const PostQuery = extendType({
5 type: 'Query',
6 definition(t) {
7 // ...
8
+ t.list.field('posts', {
+ type: 'Post',
+ resolve(_root, _args, ctx) {
+ return ctx.db.posts.filter(p => p.published === true)
+ },
+ })
15
16 },
17})

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