Input Types and Mutation Payloads

Intermediate
11 min

Input Types and Mutation Payloads

A mutation with two arguments is easy to write inline. A mutation with twelve, some of them nested objects, is not. GraphQL solves this with input object types, and mature APIs pair them with a payload type for the result. After this lesson you will be able to define input types, pass them as variables, and design mutations that stay readable as they grow.

Why Input Types Exist

Object types such as Post describe data flowing out of the server. They cannot be used as arguments, because output types may contain interfaces, unions and fields with arguments, none of which make sense in an input position. The input keyword declares a type that is valid only as an argument or inside another input type:

graphql
input CreatePostInput { title: String! body: String! tags: [String!] = [] publishAt: String } type Mutation { createPost(input: CreatePostInput!): Post! }

The rules for input types:

  • Fields may be scalars, enums, lists or other input types. Object types, interfaces and unions are not allowed.
  • Fields can have default values (tags: [String!] = []); a field without a default and without ! is optional and absent when not provided.
  • An input type may reference itself only through a nullable field or a list.

Sending Input Objects with Variables

Clients pass the whole object as a single variable, which keeps the operation document stable while the values change:

graphql
mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title tags } }
json
{ "input": { "title": "Cursor pagination explained", "body": "Offset pagination breaks when rows are inserted...", "tags": ["graphql", "pagination"] } }

Validation checks the variable against the input type before any resolver runs; a missing title or a number where a string is expected is rejected with a message in the errors array. In the resolver, args.input arrives as a plain object with defaults applied:

javascript
const resolvers = { Mutation: { createPost: async (_parent, { input }, { db, user }) => { return db.posts.create({ ...input, authorId: user.id }); }, }, };

The Single input Argument Convention

Most public APIs (GitHub, Shopify, Relay-based apps) give every mutation exactly one argument named input. Adding a field never changes the mutation signature, the client passes one variable, and generated TypeScript types map one-to-one to input types. Reserve inline arguments for trivial cases such as deletePost(id: ID!).

Naming follows the mutation: createPost takes CreatePostInput, updatePost takes UpdatePostInput. For updates, make every field optional so clients send only what changed; absent fields arrive as undefined and explicit nulls as null, so the resolver can tell them apart.

Payload Types for Predictable Results

Returning the bare Post from createPost works until you need to return something else, such as validation errors or a related object. The payload pattern wraps the result in a dedicated type:

graphql
type CreatePostPayload { post: Post errors: [UserError!]! } type UserError { field: String message: String! } type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! }

Expected failures such as "title already taken" are data, not exceptions, so they are returned in errors with post set to null, and the client renders each message next to the matching form control. Unexpected failures (database down) still surface in the top-level errors array.

| Approach | Best for | Drawback | |---|---|---| | Return the object directly | Small internal APIs | No place for business errors | | Payload with errors list | Forms and public APIs | One more type per mutation | | Union result types | Strongly typed error cases | Clients must use inline fragments |

Common Mistakes

  • Reusing an output type as an argument: createPost(post: Post!) fails schema validation.
  • Making every field of an update input non-null, forcing clients to resend the whole object.
  • Throwing an exception for validation errors the user can fix; return them in the payload instead.
Quick Quiz
Question 1 of 2

Which of these can be a field of an input object type?

Key Takeaways

  • input types are the only object-like arguments; they hold scalars, enums, lists and other input types.
  • Pass input objects as a single $input variable so the operation document stays constant.
  • Input fields support default values, and validation runs before any resolver executes.
  • The single input argument convention keeps mutation signatures stable as fields are added.
  • Payload types with an errors list return expected failures as data instead of exceptions.

Next lesson: Resolvers Explained — see how every field in the schema maps to a function and how arguments, context and the parent value reach it.

Input Types and Mutation Payloads - GraphQL | CodeYourCraft | CodeYourCraft