Writing Your Own GraphQL Resolver
Query and Mutation in Magento 2
Custom Magento 2 GraphQL resolvers connect the schema, query logic and service layer. What matters most is clear types, clean resolver classes and an architecture that does not push business logic directly into the resolver.
Table of Contents
1. What a good GraphQL resolver needs to do
A GraphQL resolver in Magento 2 is the connection between schema and application logic. It decides not only which data is loaded for a field, but also how inputs are validated, services are called and results are structured on the way back. This matters a lot in Magento, because GraphQL is often used for headless frontends, Hyva-adjacent AJAX flows or external clients.
The most common architectural mistake is writing too much logic directly inside the resolver. A resolver should really act as an orchestrator: accept arguments, check context, call a service, translate errors cleanly and format the return value into the shape the schema expects. Persistence, validation and business rules belong in services or repositories instead. That is exactly what keeps a GraphQL resolver in Magento 2 testable and extensible.
For this tutorial we build a small example module with one Query and one Mutation. The Query returns a message, the Mutation stores an input through a service. That way you see both directions: read access and write access. Both use the same service layer, but different resolver classes and different schema entries.
2. Defining schema.graphqls
The starting point is etc/schema.graphqls. This is where you describe types, Query fields, Mutation fields and their arguments. If you want to build a GraphQL resolver in Magento 2, the schema should be as clear as possible. GraphQL depends on clients being able to see exactly which fields exist and which types come back.
For the example we define a type MironsoftMessage, a Query mironsoftHello and a Mutation saveMironsoftMessage. The schema does not need to be large, but it should be named in a way that makes sense from a business perspective. Query and Mutation names like getData or saveItem are technically possible, but unnecessarily vague as an API.
type MironsoftMessage {
message: String!
status: String!
}
type Query {
mironsoftHello: MironsoftMessage
@resolver(class: "Mironsoft\\GraphQlDemo\\Model\\Resolver\\HelloQuery")
}
type Mutation {
saveMironsoftMessage(message: String!): MironsoftMessage
@resolver(class: "Mironsoft\\GraphQlDemo\\Model\\Resolver\\SaveMessageMutation")
}
The schema is not just technical configuration, it is an API contract. Anyone who thinks clearly at this stage saves themselves plenty of follow-up questions and versioning problems later on. A GraphQL resolver in Magento 2 benefits greatly when types are small, descriptive and clean from a domain perspective. That matters even more when several frontends or teams use the same API.
3. Writing a Query resolver
For read access you typically implement a resolver that fulfills ResolverInterface. The resolve method receives field, context, resolve info, parent value and arguments. In practice you rarely need all of these. What matters is that you do not turn the resolver into a dumping ground for arbitrary logic. A GraphQL resolver in Magento 2 should stay minimal and delegate to a service class.
In the Query example, the resolver calls a service that returns the message. The resolver itself only shapes the return value the way the schema expects it. That keeps the business logic in one place, and it can later be reused by REST, CLI or internal code as well.
<?php
declare(strict_types=1);
namespace Mironsoft\GraphQlDemo\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Mironsoft\GraphQlDemo\Service\MessageProvider;
/**
* GraphQL query resolver for the hello message.
*/
final class HelloQuery implements ResolverInterface
{
public function __construct(
private readonly MessageProvider $messageProvider
) {}
/**
* Resolves the hello query result.
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
): array {
return [
'message' => $this->messageProvider->getMessage(),
'status' => 'success'
];
}
}
The resolver here is deliberately short. That is not a downside, it is the goal. A clean GraphQL resolver in Magento 2 is usually small. The more domain logic you find inside a resolver, the more likely it belongs in a service instead. This separation improves testability and prevents Query resolvers from turning into a second business layer over time.
4. Writing a Mutation resolver
Mutations differ from Queries mainly in that they trigger write actions. Clean validation becomes even more important here. A GraphQL resolver in Magento 2 for a Mutation should check inputs, clearly reject invalid requests and only write through services. Direct database logic inside a resolver is almost always a mistake.
In the example, the Mutation accepts a field called message. The resolver checks whether the value is present and then delegates to a service. For production code, context checks can be relevant too: Is the user logged in? Is the role allowed to perform this action? Does store context or website context need to be considered? All of that belongs in resolvers and services, not in the schema itself.
<?php
declare(strict_types=1);
namespace Mironsoft\GraphQlDemo\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Mironsoft\GraphQlDemo\Service\MessageManager;
/**
* GraphQL mutation resolver for saving a message.
*/
final class SaveMessageMutation implements ResolverInterface
{
public function __construct(
private readonly MessageManager $messageManager
) {}
/**
* Resolves the save message mutation.
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
): array {
$message = trim((string) ($args['message'] ?? ''));
if ($message === '') {
throw new GraphQlInputException(__('The "message" argument is required.'));
}
$savedMessage = $this->messageManager->save($message);
return [
'message' => $savedMessage,
'status' => 'saved'
];
}
}
The choice of exception here is not a minor detail. A GraphQL resolver in Magento 2 should throw domain errors in a way that the client can interpret clearly. For invalid input, GraphQlInputException is often a good fit. For permissions or missing entities there are other exception types. If every exception ends up as a generic error, the quality of the API suffers.
5. Service layer and error handling
Resolvers get better once they work against a service layer. This is especially true in Magento, because the same business logic is often needed across several channels: GraphQL, REST, CLI, cron or internal application code. A GraphQL resolver in Magento 2 should therefore orchestrate services rather than make decisions itself that also apply elsewhere.
In the example we use two services: a provider for read data and a manager for write logic. In real modules you would use repositories, validators, SearchCriteriaBuilder or event dispatching there instead. What matters is that the resolver itself does not decide how something is saved or which side effects are triggered.
<?php
declare(strict_types=1);
namespace Mironsoft\GraphQlDemo\Service;
/**
* Provides a message for the GraphQL query.
*/
final class MessageProvider
{
/**
* Returns the example message.
*/
public function getMessage(): string
{
return 'Hello from Magento GraphQL.';
}
}
<?php
declare(strict_types=1);
namespace Mironsoft\GraphQlDemo\Service;
/**
* Handles write operations for the GraphQL mutation.
*/
final class MessageManager
{
/**
* Saves and returns the provided message.
*/
public function save(string $message): string
{
return $message;
}
}
Error handling should also be structured. Input errors are a different thing than permission errors or system errors. For API clients, this distinction is crucial. A well-built GraphQL resolver in Magento 2 not only returns data correctly, it also fails in a controlled and understandable way.
6. Common mistakes
The most common mistakes with GraphQL in Magento look similar to those in REST, but with a few characteristics of their own. First, the schema is named vaguely and built too generically. Second, too much logic ends up directly inside the resolver. Third, exceptions are passed through without control. Fourth, no clean separation between Query and Mutation is maintained. Fifth, return values are designed as loose arrays without the schema truly describing this structure in a stable way.
Another common mistake is failing to think about N+1 patterns. Especially with more complex GraphQL fields, a resolver can quickly trigger many individual database queries. In small examples this barely shows, but in real catalog or customer queries it shows up immediately. That is why a GraphQL resolver in Magento 2 should be built not only correctly, but also with performance in mind.
Security is also often underestimated. Just because GraphQL technically feels internal does not mean it is automatically secure. Context, customer group, store, ACL-adjacent logic and sensitive fields all need to be checked cleanly. Otherwise you quickly end up with an API that is convenient internally but too exposed externally.
7. GraphQL resolver vs. REST endpoint
The choice between GraphQL and REST in Magento is not purely a matter of taste. A GraphQL resolver in Magento 2 is powerful when clients need flexible field queries and frontends should only load exactly the data they actually render. REST fits better with classic resources, stable integration flows and many existing third-party connections.
| Aspect | GraphQL Resolver | REST Endpoint |
|---|---|---|
| Client flexibility | Very high, fields can be queried selectively | Fixed output defined per endpoint |
| Definition | schema.graphqls + resolver | webapi.xml + service contract |
| Typing | Explicitly visible in the schema | Contract described more through the API and services |
| Good fit for | Headless frontends, flexible UI data | System integrations, classic API flows |
In many Magento projects both approaches exist side by side. In that case it pays off especially to centralize the domain logic in services. That way GraphQL and REST can share the same core without building duplicate rules or diverging implementations.
Mironsoft
Magento 2 GraphQL, APIs and headless architecture
Want to build your own Magento GraphQL APIs the clean way?
We build Magento 2 GraphQL resolvers with clear schemas, a clean service layer, performant data access and stable interfaces for headless frontends and integrations.
Schema design
Clear types, queries and mutations with traceable contracts
Resolvers
Lean resolver classes instead of mixed-in business logic
Performance
Services, repositories and query strategies without unnecessary N+1 effects
9. Summary
A GraphQL resolver in Magento 2 should be small, clear and service-oriented. The schema defines types and fields, resolvers only orchestrate the request, and the actual business logic stays in dedicated services. This is how Queries and Mutations end up stable, testable and extensible.
Anyone who turns resolvers into a dumping ground for logic ends up with APIs that are hard to maintain very quickly. It is better to have an architecture where schema, resolver, service layer and data access are kept separate. That fits Magento 2.4.8 and makes GraphQL usable in the long run.
GraphQL Resolver Magento 2: The Essentials at a Glance
Schema
schema.graphqls defines types, queries and mutations as a visible API contract.
Resolver
Resolvers should orchestrate, not contain the entire business logic themselves.
Services
Encapsulate domain logic in services or repositories so GraphQL and other channels can share the same core.
Errors
Deliberately distinguish input, permission and system errors and return them to clients in a fitting way.
10. FAQ: GraphQL Resolver in Magento 2
1 What is a GraphQL resolver in Magento 2?
2 What is schema.graphqls responsible for?
3 Should business logic go directly into the resolver?
4 Query and Mutation: what's the difference?
5 Which exception fits invalid input?
GraphQlInputException, so the client can clearly read the error as an input problem.