Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What should be the GraphQL mutation return type when there is no data to return?

I have an Apollo GraphQL server and I have a mutation that deletes a record. This mutation receives the UUID of the resource, calls a REST (Ruby on Rails) API and that API just returns an HTTP code of success and an empty body (204 No Content) when the deletion was successful and an HTTP error code with an error message when the deletion does not work (404 or 500, typical REST delete endpoint).

When defining a GraphQL mutation I have to define the mutation return type. What should be the mutation return type?

input QueueInput {
  "The queue uuid"
  uuid: String!
}


deleteQueue(input: QueueInput!): ????????

I can make it work with a couple of different types of returns (Boolean, String, ...) but I want to know what is the best practice because none of the returns types I tried felt right. I think it is important that on client-side after calling the mutation I have some information about what happened if things went well (API returns 204 not content) or if some error occurred (API returns 404 or 500) and ideally have some information about the error.

like image 817
Mario Avatar asked Nov 16 '19 09:11

Mario


1 Answers

A field in GraphQL must always have a type. GraphQL has the concept of null but null is not itself a type -- it simply represents the lack of value.

There is no "void" type in GraphQL. However, types are nullable by default, so regardless of a field's type, your resolver can return nothing and the field will simply resolve to null. So you can just do

type Mutation {
  deleteQueue(input: QueueInput!): Boolean #or any other type
}

Or if you want a scalar that specifically represents null, you can create your own.

const { GraphQLScalarType } = require('graphql')

const Void = new GraphQLScalarType({
  description: 'Void custom scalar',
  name: 'Void',
  parseLiteral: (ast) => null,
  parseValue: (value) => null,
  serialize: (value) => null,
})

and then do

type Mutation {
  deleteQueue(input: QueueInput!): Void
}

That said, it's common practice to return something. For deletions, it's common to return either the deleted item or at least its ID. This helps with cache-management on the client side. It's also becoming more common to return some kind of mutation payload type to better encapsulate client errors.

There's any number of fields you could include on a "payload" type like this:

type Mutation {
  deleteQueue(input: QueueInput!): DeleteQueuePayload
}

type DeleteQueuePayload {
  # the id of the deleted queue
  queueId: ID

  # the queue itself
  queue: Queue

  # a status string
  status: String

  # or a status code
  status: Int

  # or even an enum
  status: Status

  # or just include the client error
  # with an appropriate code, internationalized message, etc.
  error: ClientError

  # or an array of errors, if you want to support validation, for example
  errors: [ClientError!]!
}

DeleteQueuePayload could even be a union of different types, enabling the client to use the __typename to determine the result of the mutation.

What information you expose, however, depend on your specific needs, and what specific pattern you employ is boils down to opinion.

See here and here for additional discussion and examples.

like image 143
Daniel Rearden Avatar answered Sep 21 '22 14:09

Daniel Rearden