Queries, Mutations & Resolvers
Writing queries with arguments, mutations for writes, and how resolvers fetch the actual data.
Queries with arguments
A query's fields can take arguments, defined in the schema, letting the client parameterize what it asks for — like a function call:
type Query {
post(id: ID!): Post
posts(limit: Int = 10, status: PostStatus): [Post!]!
}
query {
post(id: "123") {
title
author {
name
}
}
}
Arguments can also be named variables, so a client application doesn't have to string-concatenate values into a query — the query's shape stays a static, reusable string, and only the variables change per request:
query GetPost($postId: ID!) {
post(id: $postId) {
title
body
author {
name
}
}
}
{ "postId": "123" }
Sending this over HTTP is a single POST to the /graphql endpoint, with the query and variables both in the JSON body:
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetPost($postId: ID!) {
post(id: $postId) {
title
author { name }
}
}
`,
variables: { postId: '123' },
}),
});
const { data } = await response.json();
console.log(data.post.title);
Mutations for writes
While Query fields are expected to be read-only and side-effect-free, mutations are the explicit, conventional way to signal an operation changes data. Syntactically a mutation looks almost identical to a query, just using the mutation keyword:
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
status
}
}
{
"input": { "title": "GraphQL Basics", "body": "...", "authorId": "7" }
}
The mutation's selection set ({ id title status }) works exactly like a query's — you choose which fields of the newly created (or updated) object you want back, in the same request that performed the write. This avoids the classic REST pattern of POST followed by a separate GET to fetch the created resource's current state.
One GraphQL convention worth knowing: when a client sends multiple mutations in one request, the server executes them serially, one after another (unlike query fields, which can be resolved in parallel) — this matters when one mutation's effect could influence another's outcome.
Resolvers: mapping schema fields to real data
A resolver is the actual function that produces the data for one field in the schema. Every field, on every type, can have a resolver — the schema alone only describes shape; resolvers describe how to get the data.
const resolvers = {
Query: {
post: async (parent, args, context) => {
// args.id comes from the query's `id` argument
return context.db.posts.findById(args.id);
},
posts: async (parent, args, context) => {
return context.db.posts.findMany({ limit: args.limit ?? 10 });
},
},
Post: {
// Resolves the `author` field on the Post type — called once per Post
// object that was fetched and had `author` included in the query
author: async (post, args, context) => {
return context.db.authors.findById(post.authorId);
},
comments: async (post, args, context) => {
return context.db.comments.findByPostId(post.id);
},
},
Mutation: {
createPost: async (parent, args, context) => {
const { title, body, authorId } = args.input;
return context.db.posts.create({ title, body, authorId });
},
},
};
Every resolver function receives four standard arguments:
| Argument | What it is |
|---|---|
parent |
The result already resolved for the parent field (e.g., the Post object, when resolving Post.author). |
args |
The arguments passed to this field in the query (e.g., { id: "123" }). |
context |
A shared object, built fresh per request, usually holding the database connection, the authenticated user, and request-scoped helpers. |
info |
Metadata about the query itself (the field name, the AST) — rarely needed outside advanced tooling. |
The key insight is that resolvers form a tree that mirrors the query: when a client asks for post { title author { name } }, the server first calls Query.post to get a post record, then — only because author was included in the query — calls Post.author with that post as its parent, and only because name was included does it read author.name (which usually doesn't even need its own resolver, since GraphQL defaults to reading a property with the same name directly off the parent object).
Common mistakes
- Writing a "mutation" as a
Queryfield (or vice versa) — while GraphQL doesn't technically forbid aQueryfield from mutating data, it breaks the convention that clients, caching layers, and tooling all rely on: query fields are assumed safe to call speculatively, in parallel, and repeatedly. - Forgetting that field resolvers only run when the client actually asks for that field — this is a feature (avoids wasted work), but it means expensive logic placed in a field resolver won't run unless a query requests it, which can be surprising if you expect a side effect to always happen.
- Doing expensive, unbatched database calls inside a per-item field resolver (like
Post.authorabove) without any batching — this is exactly the N+1 query problem, covered in the next page.
Interview questions
Q: What is a resolver, and how does it relate to the schema? A resolver is the function that actually fetches or computes the data for one specific field defined in the schema. The schema only describes the shape and types of the API; resolvers supply the real behavior — reading from a database, calling another service, or computing a derived value — for each field a client might request.
Q: How do mutations differ from queries in GraphQL, syntactically and conventionally?
Syntactically, a mutation is nearly identical to a query — it uses the mutation keyword instead of query, and still has a selection set choosing which fields of the result to return. Conventionally, mutations signal that the operation performs a write/side effect, and when multiple mutations appear in a single request, the server executes them serially (in order) rather than resolving them in parallel, since one mutation's result may depend on a prior one's effect.
Q: What arguments does a typical resolver function receive?
parent (the already-resolved result of the parent field), args (the arguments passed to this specific field in the query), context (a shared, per-request object usually holding things like the database connection and authenticated user), and info (metadata about the query itself, rarely used directly).