GraphQL in Magento 2 is not just a read API – you can add your own types, queries and mutations. A custom resolver is a PHP class responsible for fetching data for a given field or query. I show a complete implementation: GraphQL schema, resolver with authorisation, error handling and integration tests. Using a query fetching customer order history with filters as the example.
GraphQL schema – schema.graphqls
// app/code/Vendor/OrderHistory/etc/schema.graphqls
type Query {
customerOrderHistory(
filter: OrderHistoryFilterInput
pageSize: Int = 20
currentPage: Int = 1
): OrderHistoryOutput @resolver(class: "Vendor\\OrderHistory\\Model\\Resolver\\OrderHistory") @doc(description: "Get customer order history")
}
input OrderHistoryFilterInput {
status: FilterEqualTypeInput
date_from: FilterStringTypeInput
date_to: FilterStringTypeInput
min_amount: FilterRangeTypeInput
}
type OrderHistoryOutput {
items: [CustomerOrderItem]
total_count: Int
page_info: SearchResultPageInfo
}
type CustomerOrderItem {
id: ID!
increment_id: String!
status: String!
created_at: String!
grand_total: Float!
currency_code: String!
items_count: Int!
shipping_address: OrderAddress
}
type OrderAddress {
firstname: String
lastname: String
street: [String]
city: String
postcode: String
country_code: String
}
Resolver – main class
<?php
declare(strict_types=1);
namespace Vendor\OrderHistory\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\GraphQl\Model\Query\ContextInterface;
class OrderHistory implements ResolverInterface
{
public function __construct(
private \Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
private \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder,
private \Magento\Framework\Api\FilterBuilder $filterBuilder,
private \Magento\Framework\Api\SortOrderBuilder $sortOrderBuilder,
) {}
public function resolve(
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
): array {
/** @var ContextInterface $context */
if (!$context->getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(
__('Customer must be logged in to view order history')
);
}
$customerId = $context->getUserId();
$pageSize = $args['pageSize'] ?? 20;
$currentPage = $args['currentPage'] ?? 1;
if ($pageSize > 100) {
throw new GraphQlInputException(__('pageSize cannot exceed 100'));
}
$this->searchCriteriaBuilder->addFilter('customer_id', $customerId);
$this->applyFilters($args['filter'] ?? []);
$sortOrder = $this->sortOrderBuilder
->setField('created_at')
->setDescendingDirection()
->create();
$this->searchCriteriaBuilder->setSortOrders([$sortOrder]);
$this->searchCriteriaBuilder
->setPageSize($pageSize)
->setCurrentPage($currentPage);
$searchCriteria = $this->searchCriteriaBuilder->create();
$result = $this->orderRepository->getList($searchCriteria);
return [
'total_count' => $result->getTotalCount(),
'page_info' => [
'page_size' => $pageSize,
'current_page' => $currentPage,
'total_pages' => (int)ceil($result->getTotalCount() / $pageSize),
],
'items' => $this->formatOrders($result->getItems()),
];
}
private function applyFilters(array $filter): void
{
if (!empty($filter['status']['eq'])) {
$this->searchCriteriaBuilder->addFilter('status', $filter['status']['eq']);
}
if (!empty($filter['date_from']['match'])) {
$this->searchCriteriaBuilder->addFilter(
'created_at', $filter['date_from']['match'], 'gteq'
);
}
if (!empty($filter['date_to']['match'])) {
$this->searchCriteriaBuilder->addFilter(
'created_at', $filter['date_to']['match'], 'lteq'
);
}
if (!empty($filter['min_amount']['from'])) {
$this->searchCriteriaBuilder->addFilter(
'grand_total', $filter['min_amount']['from'], 'gteq'
);
}
}
private function formatOrders(array $orders): array
{
$result = [];
foreach ($orders as $order) {
$shippingAddress = $order->getShippingAddress();
$result[] = [
'id' => base64_encode('Order/' . $order->getId()),
'increment_id' => $order->getIncrementId(),
'status' => $order->getStatus(),
'created_at' => $order->getCreatedAt(),
'grand_total' => (float)$order->getGrandTotal(),
'currency_code'=> $order->getOrderCurrencyCode(),
'items_count' => (int)$order->getTotalItemCount(),
'shipping_address' => $shippingAddress ? [
'firstname' => $shippingAddress->getFirstname(),
'lastname' => $shippingAddress->getLastname(),
'street' => $shippingAddress->getStreet(),
'city' => $shippingAddress->getCity(),
'postcode' => $shippingAddress->getPostcode(),
'country_code' => $shippingAddress->getCountryId(),
] : null,
];
}
return $result;
}
}
Example query
query GetOrderHistory {
customerOrderHistory(
filter: {
status: { eq: "complete" }
date_from: { match: "2026-01-01" }
min_amount: { from: "100" }
}
pageSize: 10
currentPage: 1
) {
total_count
page_info {
current_page
total_pages
}
items {
increment_id
status
created_at
grand_total
currency_code
shipping_address {
firstname
lastname
city
}
}
}
}
Integration test for the resolver
<?php
declare(strict_types=1);
namespace Vendor\OrderHistory\Test\Integration\Model\Resolver;
use Magento\TestFramework\TestCase\GraphQlAbstract;
class OrderHistoryTest extends GraphQlAbstract
{
private const QUERY = <<<GRAPHQL
query {
customerOrderHistory(pageSize: 5) {
total_count
items {
increment_id
status
grand_total
}
}
}
GRAPHQL;
public function testGuestCannotAccessOrderHistory(): void
{
$this->expectException(\Magento\Framework\Exception\LocalizedException::class);
$this->graphQlQuery(self::QUERY);
}
/**
* @magentoDataFixture Vendor_OrderHistory::Test/Integration/_files/customer_with_orders.php
*/
public function testCustomerCanGetOrderHistory(): void
{
$headers = $this->getCustomerAuthHeaders('customer@example.com', 'password123');
$response = $this->graphQlQuery(self::QUERY, [], '', $headers);
$this->assertArrayHasKey('customerOrderHistory', $response);
$this->assertGreaterThan(0, $response['customerOrderHistory']['total_count']);
$firstOrder = $response['customerOrderHistory']['items'][0];
$this->assertArrayHasKey('increment_id', $firstOrder);
$this->assertArrayHasKey('grand_total', $firstOrder);
}
private function getCustomerAuthHeaders(string $email, string $password): array
{
$mutation = <<<MUTATION
mutation {
generateCustomerToken(email: "{$email}", password: "{$password}") {
token
}
}
MUTATION;
$response = $this->graphQlMutation($mutation);
return ['Authorization' => 'Bearer ' . $response['generateCustomerToken']['token']];
}
}
Summary
A custom resolver in Magento 2 GraphQL implements ResolverInterface with a resolve() method. Authorisation via $context->getExtensionAttributes()->getIsCustomer(). Errors via GraphQlAuthorizationException or GraphQlInputException – they appear in the errors field of the response. Define the schema in schema.graphqls – Magento automatically links it to the resolver via the @resolver directive. Integration tests via GraphQlAbstract let you test the full stack without mocking.
