Fundamentals

Mutations

The Mutation root type, and fields that create, update, or delete data.

Mutations are the schema's entry points for writing data: the fields a client calls to create, update, or delete records. mutationType defines the Mutation root, and mutationField/mutationFields add fields to it from anywhere in a schema. Here is a mutation that updates a character's biography:

builder.mutationType({
  fields: (t) => ({
    updateCharacter: t.field({
      type: Character,
      args: {
        input: t.arg({
          type: builder.inputType('UpdateCharacterInput', {
            fields: (t) => ({
              characterId: t.id({ required: true }),
              biography: t.string({ required: true }),
            }),
          }),
          required: true,
        }),
      },
      resolve: (_root, { input }, ctx) => {
        if (!ctx.user) {
          throw new Error('Not signed in');
        }
        const entry = Characters.get(Number(input.characterId));
        if (!entry) {
          throw new Error(`No character with id ${input.characterId}`);
        }
        if (entry.editorId !== ctx.user.id) {
          throw new Error("Only the entry's editor can edit it");
        }
        entry.biography = input.biography;
        return entry;
      },
    }),
  }),
});

The field takes a single input argument, an input object that groups the mutation's values (characterId and biography) into one named type. Inside the resolver, if (!ctx.user) guards on a signed-in user (see Context). The check specific to a write comes next: entry.editorId !== ctx.user.id decides whether this particular signed-in user may change this particular record. With both checks passed, the resolver updates the record and returns it. For real authorization, plugin-scope-auth moves checks like these onto the field as declarative auth scopes.

The Mutation root

mutationType defines the root, the way queryType defines the query root. Call it once, from the file that assembles your schema:

builder.mutationType({});

In a schema split across modules, mutationField adds a single field to the root and mutationFields adds several, the same cross-module merge the query root allows:

// characters.ts — registers its write on the shared root
builder.mutationField('updateCharacter', (t) =>
  t.field({
    type: Character,
    args: { input: t.arg({ type: UpdateCharacterInput, required: true }) },
    resolve: (_root, { input }, ctx) => updateCharacter(input, ctx),
  }),
);

The root is named Mutation by default; pass name to mutationType to call it something else. A schema without writes can skip the mutation root entirely; of the three root types, only the query root is required by GraphQL's schema validation.

When a client sends several mutation fields in one operation, the graphql-js executor runs them one at a time in the order written, each finishing before the next starts. (Query root fields resolve in parallel.)

Returning what changed

Return the entity the mutation changed. Because the field's type is the object type, the client selects fields on the result and reads the new state in the same round trip:

mutation UpdateCharacter {
  updateCharacter(input: { characterId: "1", biography: "Bearer of the One Ring and hero of the War of the Ring." }) {
    id
    biography
  }
}

When a mutation has no single record to hand back, such as a bulk delete or a batch import, return a small payload type carrying whatever the client needs to know, like a count of the rows affected: DeleteCharactersPayload { deletedCount: Int! }.