Input Types
Input types are the noun shape for mutations. They keep arguments composable, type-safe, and easy to evolve.
GraphQL — input types
EXAMPLE
# ===== The problem =====
# Mutation arguments can be N positional fields. That gets ugly fast:
type Mutation {
createOrder(
customerId: ID!
line1Sku: String!
line1Qty: Int!
line2Sku: String
line2Qty: Int
note: String
): Order!
}
# Hard to evolve, hard to validate, no reuse.
# ===== The shape: input type =====
input OrderLineInput {
sku: String!
qty: Int!
}
input CreateOrderInput {
customerId: ID!
lines: [OrderLineInput!]!
note: String
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
}
# Clients send:
# mutation {
# createOrder(input: {
# customerId: 'c-1',
# lines: [{ sku: 'A-1', qty: 2 }, { sku: 'B-2', qty: 1 }],
# note: 'gift'
# }) { id total }
# }
# ===== Rules =====
# 1. Input types CANNOT have fields of object/interface/union types.
# 2. Input types CAN nest other input types and lists of them.
# 3. Input fields can be required (!) or optional.
# 4. Input types are independent of output types — design them for the mutation.
# ===== Resolver (apollo / yoga / mercurius) =====
const resolvers = {
Mutation: {
createOrder: async (_parent, { input }, ctx) => {
const total = input.lines.reduce((s, l) => s + l.qty * await ctx.price(l.sku), 0);
const order = await ctx.db.orders.create({
data: {
customer_id: input.customerId,
note: input.note ?? null,
total_cents: total,
lines: { createMany: { data: input.lines } },
},
});
return order;
},
},
};
# ===== Evolution: never break clients =====
# Add optional fields with sensible defaults; never reuse or remove required ones.
input CreateOrderInput {
customerId: ID!
lines: [OrderLineInput!]!
note: String
giftWrap: Boolean = false # added in v2; old clients unaffected
}
# ===== Patterns to internalise =====
# - One input type per mutation, named <Verb><Subject>Input
# - Input types model intent, NOT storage shape
# - Required vs optional should mirror the business rule, not the DB schema
# - Lists of inputs let you batch (create many in one round trip)
# - Validate in the resolver AND with custom scalars/directives where possible
# ===== Pitfalls =====
# - Reusing the output Order type as input -> not allowed; input is a distinct kind
# - Promoting every existing field of a type into the input -> leaks storage shape
# - Optional everywhere -> resolver has to defensively handle every combination
# - Versioning by suffix (CreateOrderInputV2) -> avoid; evolve the existing input with optionals
# - Returning bools from mutations -> return the affected entity so clients update caches
Why it matters
Input types are how you keep the mutation API tidy as the system grows. One input per mutation, model the intent (not the storage), evolve by adding optionals. The clients never break, the resolvers stay readable, and the schema doc tells the story of what the system can do.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
input UserInput { name: String! email: String! }
type Mutation { createUser(input: UserInput!): User! }
Try it Yourself »
Discussion
Loading…