Skip to content

Latest commit

 

History

History
606 lines (458 loc) · 13.7 KB

04-chapter-3-adding-mutations-to-your-api.mdx

File metadata and controls

606 lines (458 loc) · 13.7 KB
title
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
// api/db.ts

export interface Post {
  id: number
  title: string
  body: string
  published: boolean
}

export interface Db {
  posts: Post[]
}

export const db: Db = {
  posts: [{ id: 1, title: 'Nexus', body: '...', published: false }],
}

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
// api/context.ts
import { Db, db } from './db'

export interface Context {
  db: Db
}

export const context = {
  db
}

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

<TabbedContent tabs={['Diff', 'Code']}>

// api/server.ts
import { ApolloServer } from 'apollo-server'
+import { context } from './context'
import { schema } from './schema'

export const server = new ApolloServer({
  schema,
+  context 
})
// api/server.ts
import { ApolloServer } from 'apollo-server'
import { context } from './context'
import { schema } from './schema'

export const server = new ApolloServer({
  schema,
  context
})

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/app.ts.

<TabbedContent tabs={['Diff', 'Code']}>

// api/schema.ts
import { makeSchema } from '@nexus/schema'
import { join } from 'path'
import * as types from './graphql'

export const schema = makeSchema({
  types,
  outputs: {
    typegen: join(__dirname, '..', 'nexus-typegen.ts'),
    schema: join(__dirname, '..', 'schema.graphql')
  },
+  typegenAutoConfig: {
+    sources: [
+      {
+        source: join(__dirname, "./context.ts"),        // 1
+        alias: "ContextModule",                         // 2
+      },
+    ],
+    contextType: "ContextModule.Context",               // 3
+  },
})
// api/schema.ts
import { makeSchema } from '@nexus/schema'
import { join } from 'path'
import * as types from './graphql'

export const schema = makeSchema({
  types,
  outputs: {
    typegen: join(__dirname, '..', 'nexus-typegen.ts'),
    schema: join(__dirname, '..', 'schema.graphql')
  },
  typegenAutoConfig: {
    sources: [
      {
        source: join(__dirname, "./context.ts"),        // 1
        alias: "ContextModule",                         // 2
      },
    ],
    contextType: "ContextModule.Context",               // 3
  },
})
  1. Behind the scenes Nexus will parse the context.ts file to extract the available types.
  2. These types become available under the umbrella of our ContextModule alias.
  3. We can then use that alias to refer to any type defined in this file. Here, our context type is named Context, so we refer to it as ContextModule.Context.

Use the context

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

<TabbedContent tabs={['Diff', 'Code']}>

// api/graphql/Post.ts

export const PostQuery = extendType({
  type: 'Query',
  definition(t) {
    t.list.field('drafts', {
      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
      },
    })
  },
})
// api/graphql/Post.ts

export const PostQuery = extendType({
  type: 'Query',
  definition(t) {
    t.list.field('drafts', {
      type: 'Post',
      resolve(_root, _args, ctx) {                             // 1
       return ctx.db.posts.filter(p => p.published === false)  // 2
      },
    })
  },
})
  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.

// api/graphql/Post.ts
// ...

export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.nonNull.field('createDraft', {
      type: 'Post',
      resolve(_root, args, ctx) {
        ctx.db.posts.push(/*...*/)
        return // ...
      },
    })
  },
})
Mutation {
  createDraft: Post!
}

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.

<TabbedContent tabs={['Diff', 'Code']}>

import { objectType, extendType, stringArg } from '@nexus/schema'
+import { objectType, extendType, stringArg, nonNull } from '@nexus/schema'

export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.nonNull.field('createDraft', {
      type: 'Post',
+      args: {                                        // 1
+        title: nonNull(stringArg()),                 // 2
+        body: nonNull(stringArg()),                  // 2
+      },
      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 // ...
      },
    })
  },
})
import { objectType, extendType, stringArg, nonNull } from '@nexus/schema'

export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.nonNull.field('createDraft', {
      type: 'Post',
      args: {                                        // 1
        title: nonNull(stringArg()),                 // 2
        body: nonNull(stringArg()),                  // 2
      },
      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
      },
    })
  },
})
Mutation {
-  createDraft: Post
+  createDraft(title: String!, body: String!): Post!
}
  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.

<TabbedContent tabs={['Diff', 'Content', 'SDL']}>

// api/graphql/Post.ts
-import { objectType, extendType, stringArg, nonNull } from '@nexus/schema'
+import { objectType, extendType, stringArg, nonNull, intArg } from '@nexus/schema'

export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    // ...
+    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
+      },
+    })
  },
})
// api/graphql/Post.ts
import { objectType, extendType, stringArg, nonNull, intArg } from '@nexus/schema'

export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    // ...
    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
      },
    })
  },
})
type Mutation {
  createDraft(body: String!, title: String!): Post!
+  publish(draftId: Int!): Post
}

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

<TabbedContent tabs={['Diff', 'Content', 'SDL']}>

// api/graphql/Post.ts
import { extendType } from '@nexus/schema'

export const PostQuery = extendType({
  type: 'Query',
  definition(t) {
    // ...

+    t.list.field('posts', {
+      type: 'Post',
+      resolve(_root, _args, ctx) {
+        return ctx.db.posts.filter(p => p.published === true)
+      },
+    })

  },
})
// api/graphql/Post.ts
import { extendType } from '@nexus/schema'

export const PostQuery = extendType({
  type: 'Query',
  definition(t) {
    // ...

    t.list.field('posts', {
      type: 'Post',
      resolve(_root, _args, ctx) {
        return ctx.db.posts.filter(p => p.published === true)
      },
    })

  },
})
type Query {
  drafts: [Post]!
+  posts: [Post]
}

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):

mutation {
  publish(draftId: 1) {
    id
    title
    body
    published
  }
}
{
  "data": {
    "publish": {
      "id": 1,
      "title": "Nexus",
      "body": "...",
      "published": true
    }
  }
}

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

query {
  posts {
    id
    title
    body
    published
  }
}
{
  "data": {
    "posts": [
      {
        "id": 1,
        "title": "Nexus",
        "body": "...",
        "published": true
      }
    ]
  }
}

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 →