In this article we’ll be looking at using the Mutation
and Subscription
types to manipulate and watch our data for changes, instead of just querying, in GraphQL. Feel free to discover more in the official docs.
To keep things simple, we won’t be using any databases or HTTP requests, but knowing how to set up a basic API with schemas and resolvers is necessary.
We’ll be using the graphql-yoga library to setup our server and nodemon
to have it reload automatically. We’ll also need a pre-prossesor like Prepros or babel so we can use JavaScript’s latest features.
$ npm i graphql-yoga nodemon
Besides our server setup we just have an empty users
array and a simple schema and resolver for returning all our users.
import { GraphQLServer } from 'graphql-yoga'
const users = [];
const typeDefs = `
type Query {
users: [User!]!
}
type User {
name: String!
age: Int!
}
`;
const resolvers = {
Query: {
user() {
return users;
}
}
}
const server = new GraphQLServer({ typeDefs, resolvers });
server.start(() => console.log('server running'));
We’ll need a start
script that’ll run nodemon on our output file:
{
"name": "graphql-api",
"version": "1.0.0",
"description": "",
"main": "server.js",
"dependencies": {
"graphql-yoga": "^1.16.7"
},
"devDependencies": {
"nodemon": "^1.19.1"
},
"scripts": {
"start": "nodemon server-dist.js"
},
"author": "",
"license": "ISC"
}
Now in the terminal you can just run npm run start
.
Over at localhost:4000
we should have GraphQL Playground up and running with a query for user { name }
returning our empty array.
The syntax for our mutations is almost the same as for our query. We just need to declare what options we want, add any arguments (if any), and declare what type should be returned when completed.
Instead of adding all of out arguments inline, it’s pretty common to break the data into its own special type called an input
type for the sake of organization. It’s a general naming convention you’ll see in tools like Prisma to name the input whatever the resolver is ending with the word input, so addUser
gets an AddUserInput
input.
const typeDefs = `
type Mutation {
addUser(data: AddUserInput): User!
}
input AddUserInput {
name: String!,
age: Int!
}
`;
Just like with queries, we can access the arguments on args
and add our new user to our array, and return them.
const resolvers = {
Query: {...},
Mutation: {
addUser(parent, args, ctx, info) {
const user = { ...args.data };
users.push(user);
return user;
}
}
}
With the syntax being so simple, it’s almost effortless to flesh out the other CRUD operations.
We’ll know which item we’re removing or updating by just searching for the user by name.
const typeDefs = `
type Mutation {
deleteUser(name: String!): User!
updateUser(name: String!, data: UpdateUserInput): User!
}
input UpdateUserInput {
name: String
age: Int
}
`
const resolvers = {
Query: { ... },
Mutation: {
deleteUser(parent, args, ctx, info) {
// We're just finding the index of the user with a matching name,
// checking if it exists, and removing that section of the array.
const userIndex = users.findIndex(user => user.name.toLowerCase() === args.name.toLowerCase());
if (userIndex === -1) throw new Error('User not found');
const user = users.splice(userIndex, 1);
return user[0];
},
updateUser(parent, args, ctx, info) {
const user = users.find(user => user.name.toLowerCase() === args.who.toLowerCase());
if (!user) throw new Error('User not found');
// This way, only the fields that are passed-in will be changed.
if (typeof args.data.name === "string") user.name = args.data.name;
if (typeof args.data.age !== "undefined") user.age = args.data.age;
return user;
}
}
}
Now over at localhost:4000
you can try this mutation and query for our array again.
mutation {
addUser(data: {
name: "Alli",
age: 48
}) {
name
age
}
}
Or in another tab:
mutation {
updateUser(name: "Alli", data: {
name: "Crusher",
age: 27
}) {
name
age
}
}
We can use the special Subscription
type to let us watch for any changes to our data. The syntax is very similar to that of queries and mutations, just add the type Subscription
, add whatever you want it to watch, and what you want returned. We’re going to return a custom type that will send us back both our changed data and tell us whether it was a create, delete, or update operation.
To use subscriptions we’re going to need to use PubSub
from graphql-yoga and initialize it before everything else. Over in our subscription resolver we’ll use a function called subscribe
which will need to return an asynchronous event, which we’ll name user
. Whenever we want to connect something to this subscription we will use this event name.
import { GraphQLServer, PubSub } from 'graphql-yoga';
const pubsub = new PubSub();
const typeDefs = `
type Subscription {
user: UserSubscription!
}
type UserSubscription {
mutation: String!
data: User!
}
`
const resolvers = {
Query: { ... },
Mutation: { ... },
Subscription: {
user: {
subscribe() {
return pubsub.asyncIterator('user');
}
}
}
}
Our subscription itself is setup and available in the generated GraphQL docs, but it doesn’t know when to fire or what to return. Back in our mutations, we’ll add pubsub.publish
to link to our user
event and pass our data back.
const resolvers = {
Query: { ... },
Mutation: {
addUser(parent, args, ctx, info) {
const user = { ...args.data };
users.push(user);
// We'll just link it to our user event,
// and return what type of mutation this is and our new user.
pubsub.publish("user", {
user: {
mutation: "Added",
data: user
}
});
return user;
},
deleteUser(parent, args, ctx, info) {
const userIndex = users.findIndex(
user => user.name.toLowerCase() === args.who.toLowerCase()
);
if (userIndex === -1) throw new Error("User not found");
const user = users.splice(userIndex, 1);
pubsub.publish("user", {
user: {
mutation: "Deleted",
data: user[0]
}
});
return user[0];
},
updateUser(parent, args, ctx, info) {
const user = users.find(
user => user.name.toLowerCase() === args.who.toLowerCase()
);
if (!user) throw new Error("User not found");
if (typeof args.data.name === "string") user.name = args.data.name;
if (typeof args.data.age !== "undefined") user.age = args.data.age;
pubsub.publish("user", {
user: {
mutation: "Updated",
data: user
}
});
return user;
}
},
Subscription: { ... }
};
Over at localhost:4000
we can open a new tab and run the following subscription. You should see a ‘listening…’ message with a little spinning wheel. Over in another tab, we can now run any of our other past mutations and our subscription will automatically return what was done and what was changed.
subscription {
user {
mutation
data {
name
age
}
}
}
I hope this was helpful in understanding how to setup your GraphQL APIs with Mutations and Subscriptions. If there were any problems in setting this up, you can always check out this repo.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!