Mutations and Subscriptions in GraphQL

Joshua Hall

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.

Installation

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

Boilerplate Setup

Besides our server setup we just have an empty users array and a simple schema and resolver for returning all our users.

server.js

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:

package.json

{
  "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.

Create Mutation

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.

server.js

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;
    }
  }
}

Delete and Update Mutations

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.

server.js

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
  }
}

Subscriptions

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.

server.js

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
    }
  }
}

Conclusion

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.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#