Queries, mutations and resolvers
How a document becomes a result: the resolver map, the per-request context, and the execution stages.
Resolvers
const resolvers = {
Query: {
post: (_parent, { id }, { db }) => db.post.findById(id),
posts: (_parent, { first = 10, after }, { db }) =>
db.post.list({ limit: first, cursor: after }),
},
Post: {
// parent is the post returned by Query.post
author: (post, _args, { loaders }) => loaders.user.load(post.authorId),
comments: (post, { first = 5 }, { db }) =>
db.comment.listByPost(post.id, { limit: first }),
},
Mutation: {
createPost: async (_parent, { input }, { db, user }) => {
if (!user) {
return { post: null, errors: [{ field: null, message: "Not signed in" }] };
}
const post = await db.post.create({ ...input, authorId: user.id });
return { post, errors: [] };
},
},
};- Every field may have a resolver. Omit one and the default resolver reads the property with the same name from the parent object.
- A resolver receives the parent value, the arguments, and the context — nothing else. That signature is the whole interface.
- The context object carries per-request state: database handles, the authenticated user, and per-request data loaders.
- A resolver may return a value, a promise, or a list of promises; the executor awaits them all and assembles the response tree.
- Keep resolvers thin. Authorisation checks and business rules belong in services the resolver calls, so REST and background jobs can reuse them.
The execution lifecycle
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
user: await authenticate(req),
db,
loaders: makeLoaders(db), // fresh loaders per request
}),
listen: { port: 4000 },
});
console.log("GraphQL ready at " + url);| Stage | What happens |
|---|---|
| Parse | The document becomes an AST; a syntax error stops here |
| Validate | Every field, argument and variable is checked against the schema |
| Execute | Resolvers run field by field, in parallel where fields are independent |
| Serialise | The response is built as a data tree plus an errors array |
| Introspect | Clients can query __schema to discover every type |
⚠️
GraphQL usually answers with HTTP 200 and an
errors array even when something failed, and a partial result plus errors is a valid response. Client code must check errors, not only the status code, or it will silently render missing data.FAQ
Where does authorisation belong?
In the resolver, or in the service it calls, applied per field or per object. There is one shared endpoint, so there is no per-route filter to attach it to.
How should a mutation report expected failures?
Model them as payload fields such as an
errors list, and reserve the top-level errors array for unexpected problems. Clients can then handle both in a typed way.Related
Schema and type system When GraphQL beats REST, and its pitfalls
Last refreshed 2026-09-18.