An Argument Is a Small Type Contract
An Argument Is a Small Type Contract
REST encodes inputs in the query string: GET /articles?limit=20&sort=newest. Every value arrives as text, every constraint lives in a doc comment, and a typo like limt=20 fails silently by returning the default result set. GraphQL arguments reuse the schema's type system, so the server rejects malformed input before any resolver executes.
type Query {
articles(
limit: Int = 20
sort: SortOrder = NEWEST
tags: [String!]
authorId: ID
): [Article!]!
}
enum SortOrder { NEWEST OLDEST MOST_LIKED }
Read that signature as four separate promises:
limit: Int = 20— the type rejects"twenty"at validation, and the default fixes behavior when the caller says nothing.sort: SortOrder— an enum turns an open-ended string into a closed set. A new sort mode becomes a schema change you review, not a typo the resolver silently absorbs.tags: [String!]— a list whose elements must be non-null. Onenullinside the array is a validation error, while omittingtagsentirely stays legal because the argument itself is nullable.authorId: ID— an optional filter; absence means "no filter," and the resolver only adds a WHERE clause when the argument is present.
The distinction between an argument's own nullability and the nullability of what it contains is the detail that trips people up. [String!]! means "required, and every element must be real"; [String] means "optional, and sloppy elements are tolerated." Write both deliberately.
By the time a resolver runs, values are already coerced: limit is a JavaScript number and sort is one of the enum's runtime values. Coercion failures arrive as a single validation error with a path pointing at the offending argument — execution never starts, so no partial work happens on bad input.
Naming That Survives Code Generation
Naming That Survives Code Generation
Client SDKs are generated from the schema, which turns naming from taste into interface design. Every inconsistency multiplies into a confusing generated method and a support question later.
Pick a scheme and hold it across every field:
| Concept | Argument name | Where it appears |
|---|---|---|
| record identity | id |
every field taking one record |
| cursor pagination | first, after |
every list field |
| offset pagination | limit, offset |
every list field (one style per schema, never both) |
| text search | query |
every search field |
| ordering | sort |
every list field |
Consistency matters more than the specific choice. If one list field takes limit and another takes first, every caller memorizes which is which, and the generated client will not save them.
Two more rules that pay off quickly:
- Arguments are nouns; the verb lives in the field name.
createArticle(input: ...), notcreateArticle(action: ...). On mutations the field name carries intent, the arguments carry data. - Default optional arguments instead of leaving behavior implied. With
limit: Int = 20, the schema states the fallback. Without it, "omitted" is whatever the resolver happens to do, and clients discover that behavior by reading server source.
When an argument outlives its usefulness, mark it rather than remove it:
articles(
limit: Int = 20
tag: String @deprecated(reason: "use tags: [String!] instead")
): [Article!]!
Removal breaks every generated client at once; deprecation surfaces a warning in tooling while old callers keep working. The wider rollout strategy lives in deprecation strategy, and the naming conventions shared with the rest of the schema are covered in schema naming conventions.
Arguments are also where pagination design starts. Once you commit to first/after, the matching cursor rules live in relay style pagination.
Omitted, null, and the Validation Boundary
Omitted, null, and the Validation Boundary
Three request shapes reach a resolver, and confusing them is a real source of bugs:
articles(limit: 20) → args.limit = 20 explicit value used
articles() → args.limit = 20 omitted, so the default applies
articles(limit: null) → args.limit = null explicit null OVERRIDES the default
That last line is the trap. A default fills absence, not null: limit is nullable, so an explicit null passes validation and reaches the resolver as null, and any limit ?? 20 guard in the resolver now disagrees with what the schema appeared to promise. Make the intent explicit in code:
const safeLimit = args.limit === null || args.limit === undefined
? 20
: Math.min(args.limit, 100);
If a value is truly required, declare it Int! — then both omission and null are rejected during validation and the resolver never defends itself.
Validation splits at a clean boundary:
- Schema validation (automatic): the argument exists, the type matches, non-null is satisfied, enum values are legal, list elements respect their nullability. No code required from you.
- Business validation (yours):
limitbetween 1 and 100,from < toin a date range, the caller allowed to filter by thatauthorId. SDL cannot express ranges or permissions, so enforce them in the resolver (or a directive that runs before it) and report failures the way input object types describes.
An interview favorite: "what is the difference between Int, Int!, and Int = 10?" Answer all three — nullable-optional, required-or-error, optional-with-fallback — then add the sharper edge case: Int! = 10 is legal. Omission is fine because the default applies, but an explicit null is still rejected, which is exactly what you want for a server-tuned fallback.
Practice Problems
Declare the arguments for a products list field. It must support free-text search, price filtering, a stable pagination style, and a capped page size, with every value validated before resolvers run. Callers that pass nothing should get a sensible default result set.
Example:
Input: { products(query: "desk lamp", priceMax: 5000, first: 10) { id title } }
Output: { "data": { "products": [ { "id": "p-1", "title": "Desk Lamp" } ] } }
priceMax is in minor units as an Int, first comes from the pagination style, and free text uses the same query name every search field in the schema uses.
Optimal Solution — O(1) time, O(1) space
Type each concern separately, default the optional ones, and express bounds the schema can express while resolvers handle numeric ranges.
type Query {
products(
query: String
priceMin: Int
priceMax: Int
first: Int = 20
after: String
sort: ProductSort = RELEVANCE
): ProductConnection!
}
enum ProductSort { RELEVANCE PRICE_ASC PRICE_DESC NEWEST } Edge Cases:
- priceMin > priceMax is legal per the schema but nonsensical — the resolver must reject it
- query: "" should be treated as no filter rather than matching nothing
- first larger than the max must be capped or rejected consistently, not silently ignored
An updateProfile field takes bio, displayName, and avatarUrl. Omitting an argument must leave the stored value untouched, while passing null must clear it. Show the SDL and the resolver branch that keeps those two cases apart.
Example:
Input: mutation { updateProfile(displayName: "Ada") { id displayName bio } }
Output: { "data": { "updateProfile": { "id": "u-1", "displayName": "Ada", "bio": "existing bio" } } }
bio was omitted, so it never reaches the update and the stored value survives.
Optimal Solution — O(1) time, O(1) space
Leave each argument nullable and undefined-able, then build the update object only from arguments that are present.
type Mutation {
updateProfile(
bio: String
displayName: String
avatarUrl: String
): User!
}
const resolvers = {
Mutation: {
updateProfile: (_p, args, ctx) => {
const patch = {};
if (args.bio !== undefined) patch.bio = args.bio; // null clears, absent skips
if (args.displayName !== undefined) patch.displayName = args.displayName;
if (args.avatarUrl !== undefined) patch.avatarUrl = args.avatarUrl;
return ctx.dataSources.users.update(ctx.userId, patch);
},
},
}; Edge Cases:
- Passing all three as absent must send no update statement at all
- A variable supplied as null behaves like an explicit null, not like omission
- If arguments were given defaults, omission would silently overwrite data — never default update fields
Quiz
1. The field is articles(limit: Int = 20). The client omits limit. What does the resolver receive?
2. Which argument declaration requires the list to be provided and forbids null elements?
3. The field declares articles(limit: Int = 20) and the client sends limit: null. What reaches the resolver?
4. Where does the rule "sort order must be one of NEWEST, OLDEST, MOST_LIKED" get enforced?
Flashcards
Question
Default value vs nullable argument
Click to reveal answer
Answer
A default fills omission only. An explicit null on a nullable argument reaches the resolver as null and does not fall back to the default.
Question
Reading list nullability
Click to reveal answer
Answer
[String!] = optional list, no null elements. [String]! = required list, null elements allowed. [String!]! = required and no null elements.
Question
Where argument validation happens
Click to reveal answer
Answer
Schema validation rejects wrong types, missing required args, and illegal enum values before execution. Ranges, formats, and permissions are resolver work.
Question
Consistent argument naming payoff
Click to reveal answer
Answer
Generated SDKs, docs, and muscle memory stay predictable: id, first/after or limit/offset (one style), query, sort — the same name for the same concept everywhere.
Question
Removing an argument without breaking clients
Click to reveal answer
Answer
Mark it @deprecated(reason: ...) first so tooling warns while old documents keep working, then remove it in a later schema version.
Revision Notes
Key Takeaways
- 1. Arguments carry the full type system, so type errors surface at validation instead of inside resolvers
- 2. Defaults apply to omitted arguments only — an explicit null overrides them
- 3. Consistent names across fields are worth more than clever names on individual fields
- 4. SDL enforces shape; numeric ranges, formats, and permissions require resolver-side validation
Interview Tips
- • Answer the "Int vs Int! vs Int = 10" question, then volunteer the Int! = 10 case: omission is allowed, explicit null is not
- • When designing a list endpoint on the spot, name the arguments you would standardize on and explain why one pagination style per schema beats supporting both
Cheat Sheet
Field Arguments Cheat Sheet
articles(
limit: Int = 20
sort: SortOrder = NEWEST
tags: [String!]
): [Article!]!
limit: Int = 20→ omitted = 20;limit: null= null (default is not used)limit: Int!→ omission or null is a validation error[X!]!required list, no null elements ·[X!]optional list ·[X]!required, nulls allowed- Standard names: id, first/after (or limit/offset), query, sort
- Args are nouns, verbs go in the field name:
createArticle(input: ...) - Deprecate with
@deprecated(reason: ...); never remove an argument cold - Type/nullability/enum checks = validation; ranges and permissions = resolver