{ The Rithm Blog. }

An Introduction to GraphQL - Mutations December 02, 2019

Welcome! If you haven't seen our first GraphQL tutorial on queries, make sure to check that out first. In this post, we'll talk more about GraphQL and focus on mutations. We previously saw how to access or read data using queries, now let's see how to change or "mutate" information using mutations! Make sure that you have a fundamental understanding of GraphQL and queries before continuing on.

What is a mutation?

Like we mentioned earlier, a mutation is how we change information using GraphQL. Mutations are used when we need to create, update or delete data and they're called mutations because something is being mutated or changed.

What's nice about GraphQL is that once you get a grasp on the syntax of queries and mutations, you're almost all of the way towards consuming GraphQL servers!

It's sometimes challenging to get up to speed with mutations because there are not many open APIs, but thankfully we have one right here!

Getting up to speed

We're going to be working with a very small API which is backed by a Postgres database with two tables, users and messages. We have a one to many relationship here, so one user can write many messages and each message is written by a single user.

There is no authentication in the application so any user can delete any message they would like. If we wanted to add some authentication, our GraphQL endpoints would still remain the same, we would just be sending a cookie or token in the headers, so adding auth does not impact what our GraphQL schema looks like.

If you'd like to see the code for this application, you can view it here

Types

Let's first start by taking a look at the custom types we have in our application. Like we mentioned, we are working with users and messages, so let's see what these types consist of:

User

type User {
  username: ID!
  first_name: String!
  last_name: String!
  messages: [Message!]
}

You can see that each user has a required username which is unique, a first name and last name which are strings, and an array of message types. You can think of this like a one to many relationship where one user has many messages.

Message

type Message {
  id: ID!
  body: String!
  user: User!
}

You can see that each message has a required id which is unique, a message body which is a string and a property called user which is a singular User type. You can think of this like the other side of a one to many relationship where each message belongs to a single user.

Queries

If you take a look at the docs, you will see that there are four queries that we can use, let's examine those further.

  • users - returns an array of user types
  • user - accepts a username and returns a user type
  • message - accepts a message id and returns a message type
  • messages - accepts a username and returns an array of message type

Before you take a look at the mutations, try making a few queries and playing around with these four ones!

Mutations

If you take a look at the docs, you will see that there are also three mutations that we can use, let's examine those further.

  • createUser(username: ID!, first_name: String!, last_name: String!): User!
  • createMessage(username: ID!, body: String!): Message!
  • deleteMessage(id: ID!): Message

Here you see the name of each mutation and the parameters and types that are expected in parenthesis and after the colon, you can see what the mutation returns. Let's start with our first mutation!

createUser - accepts a username, first name and last name and returns the user created or an error if the username already exists

Let's see this in action

mutation {
  createUser(username:"sample", first_name:"first test", last_name:"last test"){
    username
    first_name
    last_name
  }
}

Returns the following to us:

{
  "data": {
    "createUser": {
      "username": "sample",
      "first_name": "first test",
      "last_name": "last test"
    }
  }
}

If we make this request again, we will see:

{
  "errors": [
    {
      "message": "duplicate key value violates unique constraint \"users_pkey\"",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "name": "error",
          "length": 191,
          "severity": "ERROR",
          "code": "23505",
          "detail": "Key (username)=(sample) already exists.",
          "schema": "public",
          "table": "users",
          "constraint": "users_pkey",
          "file": "nbtinsert.c",
          "line": "534",
          "routine": "_bt_check_unique"
        }
      }
    }
  ],
  "data": null
}

The reason for this is that we have a primary key on the username column so we do not allow for duplicate usernames. We could make this error message a bit more user friendly by tweaking the back-end so don't be alarmed by the message here.

Now let's see how to create a message using the createMessage mutation. This mutation accepts a username that is a string and a body which is also a string. It either returns the new message created or an error if the username does not exist.

mutation {
  createMessage(username:"sample", body:"another message by me!"){
    id
    body
  }
}

When we make this request again, we see (your id will be different):

{
  "data": {
    "createMessage": {
      "id": "13",
      "body": "another message by me!"
    }
  }
}

Now that you have seen these two mutations, try using the deleteMessage mutation which accepts just a message ID to delete a message!

Good practice with mutations

There are a few standard conventions that you should take advantage of when working with mutations.

  1. Name your mutations verb first and use camelCase. This means you should call your mutations things like updateTodo, removeUser etc.

  2. Make your mutations as specific as possible. If your action involves creation, updating or deletion, make sure that the verb is part of the name of your mutation.

Why not just have a query for everything?

As you learn about mutations, you might be wondering why we don't just have queries for everything and why mutations exist. According to the docs:

In REST, any request might end up causing some side-effects on the server, but by convention it's suggested that one doesn't use GET requests to modify data. GraphQL is similar - technically any query could be implemented to cause a data write. However, it's useful to establish a convention that any operations that cause writes should be sent explicitly via a mutation.

Simply put, it's nice to have convention when discussing different kinds of operations and in this case, that means just calling these different types of operations different things.

Making API calls using fetch

Before we go, let's take the knowledge we have now from our Graphcool playground, and use some client-side JavaScript to make a mutation using fetch!

Below, we'll make a simple AJAX request and console.log the response we get back, try this out in the Chrome console!

fetch('https://users-messages-gql.herokuapp.com/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: ` mutation { 
      createUser(first_name: "Elie", last_name:"Schoppik", username:"eschop"){
        first_name
        last_name
        username
      }
    }` 
  }),
})
  .then(res => res.json())
  .then(res => console.log(res.data.createUser));

You can always change the values in the mutation if you are getting a duplicate key error. Try this out with all of the mutations you've seen so far!

Input Objects

As you learn more about mutations, you will come across another common convention, which is that mutations should only ever have one argument - an input type.

In many situations, you will find that multiple mutations that all accept the same input parameters, a common example being creating and updating some data.

To make your schema simpler, you can use “input types” for this, by using the input keyword instead of the type keyword. Here is what a message input type could look like:

input MessageInput {
  username: ID!
  body: String!
}

If we decide to add or update messages we can now use the same input! Another advantage here is that we can go from the following:

createMessage(input: { id: 4, body: "..." })

To:

createMessage(id: 4, body: "...")

Why is this also helpful? A big reason is that it's easier on the client side. The client is only required to send one variable with per mutation instead of one for every argument on the mutation.

Next Steps

Now that we've seen queries and mutations, the next step is to practice and keep learning! In the next post, we'll discuss subscriptions and go more in depth with queries and mutations. Until then, you can read all about mutations here.

Written by Elie

Back to all posts

Get Started with Rithm School