Resolvers and GraphQL Execution
Understand resolver functions, root resolvers, nested resolvers, resolver arguments, parent values, context, default resolution, execution order, and the relationship between schemas and application logic.
What Are Resolvers?
Resolvers are functions that provide the values requested by GraphQL operations.
Consider this schema:
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
A resolver implementation might conceptually look like:
const resolvers = {
Query: {
user: async (_, args) => {
return userRepository.findById(args.id);
}
}
};
The schema defines the contract. The resolver provides the behavior.
A resolver can retrieve data from almost anywhere:
- MongoDB
- PostgreSQL
- Redis
- Another REST API
- Another GraphQL API
- An in-memory cache
- A message or event system
- A computed value
Resolvers therefore act as an important boundary between the GraphQL API layer and the application's domain/data layers.
A common architectural mistake is putting all business logic directly inside resolver functions. Resolvers should generally coordinate the request rather than becoming enormous business-service functions.
const resolvers = {
Query: {
user: async (_, args) => {
return userRepository.findById(args.id);
}
}
};user resolver receives the field arguments and uses them to obtain a user. The resolver does not need to know how the repository stores the data. Keeping persistence behind a repository or service makes the GraphQL layer easier to maintain and test.- Resolvers provide values for schema fields.
- Resolvers can call any appropriate data source.
- The schema defines the contract while resolvers provide behavior.
- Complex business logic is usually better placed in service/domain layers.
What is the relationship between a GraphQL schema and a resolver?
Resolver Function Arguments
A resolver function commonly receives four conceptual arguments:
(parent, args, context, info)
The exact function signature can vary by GraphQL server implementation, but these four concepts are fundamental.
Parent
parent contains the value returned by the resolver of the parent field.
Args
args contains arguments supplied to the field.
For:
user(id: "42")
args might be:
{ id: "42" }
Context
context contains request-scoped information shared by resolvers. It is commonly used for authentication information, database clients, loaders, request IDs, or other request-level dependencies.
Info
info contains execution metadata, including information about the current field and GraphQL operation. Advanced tooling and optimization systems can use it.
Example:
const resolvers = {
Query: {
user: async (parent, args, context, info) => {
return context.userService.findById(args.id);
}
}
};
Understanding these arguments is essential because they provide the bridge between the GraphQL execution engine and your application.
Value returned by the parent resolverArguments supplied to the fieldRequest-scoped application contextGraphQL execution metadata- Resolvers commonly receive parent, args, context, and info.
- Args contain field arguments.
- Context is useful for request-scoped dependencies.
- Parent is especially important for nested object resolution.
Where would you normally find the value supplied to a field argument such as `user(id: "42")`?
Nested Resolvers and Parent Values
GraphQL fields can contain nested objects, and nested fields can have their own resolvers.
Consider:
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
}
A query may request:
query {
user(id: "42") {
id
name
posts {
id
title
}
}
}
The Query.user resolver can return a user object. The User.posts resolver can then use that returned object as its parent value.
const resolvers = {
Query: {
user: (_, { id }, context) => context.userService.findById(id)
},
User: {
posts: (user, _, context) => {
return context.postService.findByUserId(user.id);
}
}
};
This is one of the most important concepts in GraphQL execution.
The parent resolver does not necessarily have to fetch every nested relation immediately. Each nested field can resolve its own data.
This flexibility can become a performance problem if implemented naively. If a query requests 100 users and each user's posts resolver performs a separate database query, the server can encounter the classic N+1 query problem. DataLoader and batching techniques will be covered later in the course.
- Nested fields can have their own resolvers.
- The parent argument contains the value returned by the parent field.
- Nested resolvers enable flexible data composition.
- Naive nested resolution can produce N+1 database queries.
In a `User.posts` resolver, what does the `parent` argument commonly contain?
Default Field Resolution
Not every field needs an explicit resolver.
Suppose a resolver returns:
{
id: "42",
name: "Alice",
email: "alice@example.com"
}
and the schema contains:
type User {
id: ID!
name: String!
email: String!
}
GraphQL implementations commonly provide default field resolution behavior that reads matching properties from the parent object.
Conceptually:
User object
|
+-- id → User.id
+-- name → User.name
+-- email → User.email
Therefore, you often only need to explicitly define a resolver for fields requiring special behavior.
For example, if name is already a property on the returned object, an explicit resolver may be unnecessary.
But if the schema field is:
fullName: String!
while the underlying object contains:
{
firstName: "Alice",
lastName: "Johnson"
}
you may need a custom resolver:
const resolvers = {
User: {
fullName: (user) => `${user.firstName} ${user.lastName}`
}
};
This default-resolution behavior helps keep resolver maps concise.
const resolvers = {
User: {
fullName: (user) => `${user.firstName} ${user.lastName}`
}
};fullName property, so a custom resolver computes it from firstName and lastName. Fields whose names already correspond to parent object properties can often use default resolution.- GraphQL servers commonly provide default field resolution.
- Explicit resolvers are useful for computed or transformed fields.
- Returning an object with matching properties can reduce resolver boilerplate.
When can a GraphQL field often use default resolution instead of a custom resolver?
Resolver Execution and Parallelism
GraphQL execution is field-oriented, but the exact execution behavior depends on the operation and server implementation.
A useful conceptual model is that sibling fields can often be resolved independently, while fields that depend on parent objects are resolved after their parent values are available.
For example:
query {
user(id: "42") {
id
name
posts {
id
title
}
}
}
The user field must produce the parent object before posts can use that user value. Once an object is available, its selected child fields can be evaluated according to GraphQL's execution semantics.
This matters when writing asynchronous resolvers.
For example:
const resolvers = {
Query: {
dashboard: async (_, __, context) => {
const [profile, notifications] = await Promise.all([
context.profileService.getProfile(),
context.notificationService.getNotifications()
]);
return { profile, notifications };
}
}
};
Application-level concurrency can reduce latency when independent data sources can safely be queried in parallel.
However, parallel execution is not a reason to fire unlimited database queries. Connection pools, downstream service limits, rate limits, CPU, memory, and query complexity must all be considered.
Later chapters will cover DataLoader, batching, caching, query complexity, and performance controls.
Running independent operations concurrently can reduce latency, but unrestricted concurrency can overload databases and downstream services. Production GraphQL systems need deliberate concurrency controls.
- Nested fields depend on their parent values.
- Independent asynchronous work can sometimes be performed concurrently.
- Uncontrolled concurrency can overload downstream systems.
- GraphQL performance requires coordination between execution and data-source limits.
Why should a GraphQL server avoid treating resolver concurrency as unlimited?