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.
$
1// api/db.ts23export interface Post {4 id: number5 title: string6 body: string7 published: boolean8}910export interface Db {11 posts: Post[]12}1314export 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:
- Pass the
db
object to our GraphQL server context - 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.
$
1// api/context.ts2import { Db, db } from './db'34export interface Context {5 db: Db6}78export const context = {9 db10}
Then we'll pass our in-memory database to our GraphQL server
1// api/server.ts2import { ApolloServer } from 'apollo-server'+import { context } from './context'4import { schema } from './schema'56export const server = new ApolloServer({7 schema,+ context9})
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
.
1// api/schema.ts2import { makeSchema } from 'nexus'3import { join } from 'path'4import * as types from './graphql'56export 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})
- Option to set the context type
- Path to the module where the context type is exported
- 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.
1// api/graphql/Post.ts23export 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) // 212 },13 })14 },15})
- Context is the third parameter, usually identified as
ctx
- 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.ts23export 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.
1import { objectType, extendType } from 'nexus'+import { objectType, extendType, stringArg, nonNull } from 'nexus'34export 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})
- Add an
args
property to the field definition to define its args. Keys are arg names and values are type specifications. - Use the Nexus helpers for defining an arg type. There is one such helper for every GraphQL scalar such as
intArg
andbooleanArg
. If you want to reference a type like some InputObject then usearg({ type: "..." })
. You can use the helpersnonNull
andnullable
to adjust the nullability type of the arg. You can use the functional helperlist
to turn the arg into a list type too. - 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.
1// api/graphql/Post.ts-import { objectType, extendType, stringArg, nonNull } from 'nexus'+import { objectType, extendType, stringArg, nonNull, intArg } from 'nexus'45export 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.
1// api/graphql/Post.ts2import { extendType } from 'nexus'34export 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)+ },+ })1516 },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):
12345678
1{2 "data": {3 "publish": {4 "id": 1,5 "title": "Nexus",6 "body": "...",7 "published": true8 }9 }10}
Now, that published draft should be visible via the posts
Query. Run this query (left) and expect the following response (right):
12345678
1{2 "data": {3 "posts": [4 {5 "id": 1,6 "title": "Nexus",7 "body": "...",8 "published": true9 }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 →