Writing Your Own GraphQL Resolver in Magento 2 | Query and Mutation
AI generated
Magento 2 · GraphQL

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.

14 min read PHP 8.4 Magento 2.4.8

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?
It processes a Query or Mutation and connects the GraphQL schema with service or data logic.
2 What is schema.graphqls responsible for?
Types, queries, mutations, arguments and the mapping to resolver classes.
3 Should business logic go directly into the resolver?
No. Resolvers should delegate to services and stay as lean as possible themselves.
4 Query and Mutation: what's the difference?
Queries read data. Mutations change data or trigger write processes.
5 Which exception fits invalid input?
Often GraphQlInputException, so the client can clearly read the error as an input problem.
6 Is a resolver allowed to use repositories?
Yes. Repositories and services are typical DI dependencies for resolvers.
7 When is GraphQL better than REST?
When clients need to flexibly load only exactly the fields they need.
8 What is a typical performance mistake?
N+1-style queries caused by many small data accesses within nested field resolutions.
9 Does a mutation need validation?
Yes. Inputs, permissions and side effects must be deliberately checked.
10 How do you test a GraphQL resolver?
With a real GraphQL query plus tests of the service layer, inputs and error cases.