Mutations

In chapter 10, we saw how the shop’s REST API had problems with under-fetching and over-fetching of data. The GraphQL API fixes that by letting clients ask for exactly the data they need. We designed the types and queries in the schema to do that.

In GraphQL, a request that changes data is a mutation. Like queries, mutations are declared in the schema, in a type named Mutation. Its fields define the entry points for writing data.

Here is an example mutation for placing an order:

type Mutation {
  placeOrder(cartId: ID!, idempotencyKey: String!): Order
}

It takes a cart ID and an idempotency key and places the order. It returns an Order, or null if the mutation fails.

The request

A mutation is sent to the same endpoint, POST /graphql, and its text begins with the word mutation.

Here is an example request for placing an order:

mutation {
  placeOrder(
    cartId: 73
    idempotencyKey: "6f1c2a9e-3d4b-4e58-9a10-7b2c5d8e1f03"
  ) {
    id
    totalCents
    items {
      productName
      unitPriceCents
      quantity
    }
  }
}

After the arguments, there is a selection, as in a query. The client says which fields of the returned Order it wants.

Here is an example response:

{
  "data": {
    "placeOrder": {
      "id": "91",
      "totalCents": 7300,
      "items": [
        {
          "productName": "Blue mug",
          "unitPriceCents": 1800,
          "quantity": 2
        },
        {
          "productName": "White mug",
          "unitPriceCents": 1500,
          "quantity": 1
        },
        {
          "productName": "Speckled mug",
          "unitPriceCents": 2200,
          "quantity": 1
        }
      ]
    }
  }
}

The result is under data, keyed by the mutation’s name, in the shape of the selection.

The idempotency key

We learned in chapter 9 to use an Idempotency-Key header with POST /notes requests to avoid creating duplicate notes when the client retries the request.

The shop needs the same guarantee for orders. Here, placeOrder takes the key as an argument, idempotencyKey.

The key is passed as an argument instead of a request header because one GraphQL request can hold several mutations. A header belongs to the whole HTTP request, so it would apply to every mutation in it. But each mutation needs its own key.

If the client sends placeOrder again with the same key and the same cart, the API returns the existing order and does not create another. If it sends the same key with a different cart, it gets an error with the code KEY_REUSED. This is the same design as the Idempotency-Key header in REST.

The shop’s mutations

The shop has eight mutations, one for each write in the REST version of the API.

Who Mutation REST
merchant createProduct POST /products
merchant updateProduct PUT and PATCH /products/42
merchant adjustStock POST /products/42/stock
merchant deleteProduct DELETE /products/42
shopper createCart POST /carts
shopper setCartItem PUT /carts/73/items/42
shopper removeCartItem DELETE /carts/73/items/42
shopper placeOrder POST /orders

In REST, the URL names a resource and the HTTP method names the action. In GraphQL, the mutation’s name says the action. PUT and PATCH on a product become one updateProduct with optional arguments to handle both full and partial updates.

Resolvers

The schema declares what a client may ask for. It is a specification, and the server has to implement it. Each field in the schema corresponds to a resolver, a function the server uses to fetch or compute the value of that field. GraphQL execution is defined field by field. For each field in the selection, the server calls the resolver with the parent object and the field’s arguments, and the resolver returns the value. The developer writes resolvers in the server code.

Every GraphQL server library has a default resolver. If you do not write one for Product.name, for example, the default resolver reads the property called name from whatever object the parent resolver returned. So in practice you write resolvers for the fields of Query and Mutation, and for any field on a type whose value is not already on the parent object. In the shop, those would be fields like CartItem.product, which needs a lookup by productId, and Cart.totalCents, if it is computed rather than stored.

So for a mutation like placeOrder, the server calls the placeOrder resolver, which carries out the operation: it checks stock, records the order, reduces stock, and marks the cart as ordered, all in one transaction. That is the same work POST /orders does.

We will leave resolvers there, since they belong to the server implementation rather than to the API design.