How to create a GraphQL Mutation Endpoint for Magento
GraphQL in Magento isn't just for fetching data. Honestly, the real power emerges when you let the frontend modify the database. This means building a mutation. A previous article covered creating a basic query endpoint for a custom pickup store module. Now we’ll allow the creation of new stores.
We think the current Magento GraphQL implementation’s authorization is rudimentary, limited to customer tokens for querying customer data. For clarity, this example deliberately skips access controls. This is a demonstration piece, not production-ready code. You must layer in proper security yourself.
You need the base module from the prior tutorial as a starting point. Grab it from the GitHub repository.
The Resolver: Orchestrating the Mutation
Every GraphQL endpoint in Magento requires a resolver. This class acts as the controller. It implements ResolverInterface and its job is to validate incoming arguments and delegate the business logic. The mutation resolver for our pickup store creation looks like this.
GraphQLStorePickup/Model/Resolver/CreatePickUpStore.php
<?php
declare(strict_types=1);
namespace Vendor\GraphQLStorePickup\Model\Resolver;
use Vendor\GraphQLStorePickup\Model\CreatePickUpStoreService;
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;
class CreatePickUpStore implements ResolverInterface
{
/**
* @var CreatePickUpStoreService
*/
private $createPickUpStoreService;
/**
* @param CreatePickUpStoreService $createPickUpStoreService
*/
public function __construct(CreatePickUpStoreService $createPickUpStoreService)
{
$this->createPickUpStoreService = $createPickUpStoreService;
}
/**
* @inheritDoc
*/
public function resolve(Field $field, $context, ResolveInfo $info, ?array $value = null, ?array $args = null)
{
// Validate the mandatory 'input' argument
if (empty($args['input']) || !is_array($args['input'])) {
throw new GraphQlInputException(__('The "input" argument must be provided as an array.'));
}
// Delegate to the service class and format the response
return ['pickup_store' => $this->createPickUpStoreService->execute($args['input'])];
}
}
The resolver is intentionally thin. Its primary function is to ensure the required input data exists before passing it along. According to our data, this separation keeps the GraphQL layer clean and testable.
The Service Class: Encapsulating Business Logic
The heavy lifting happens in a dedicated service class. This pattern isolates the creation logic, making it reusable across different entry points like CLI commands or REST APIs. The service validates data, creates a data transfer object, and persists it.
GraphQLStorePickup/Model/CreatePickUpStoreService.php
<?php
declare(strict_types=1);
namespace Vendor\GraphQLStorePickup\Model;
use Vendor\GraphQLStorePickup\Api\Data\StoreInterface;
use Vendor\GraphQLStorePickup\Api\Data\StoreInterfaceFactory;
use Vendor\GraphQLStorePickup\Api\StoreRepositoryInterface;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
class CreatePickUpStoreService
{
/**
* @var DataObjectHelper
*/
private $dataObjectHelper;
/**
* @var StoreRepositoryInterface
*/
private $storeRepository;
/**
* @var StoreInterfaceFactory
*/
private $storeFactory;
/**
* @param DataObjectHelper $dataObjectHelper
* @param StoreRepositoryInterface $storeRepository
* @param StoreInterfaceFactory $storeFactory
*/
public function __construct(
DataObjectHelper $dataObjectHelper,
StoreRepositoryInterface $storeRepository,
StoreInterfaceFactory $storeFactory
) {
$this->dataObjectHelper = $dataObjectHelper;
$this->storeRepository = $storeRepository;
$this->storeFactory = $storeFactory;
}
/**
* Main execution method. Orchestrates validation, creation, and persistence.
*
* @param array $storeData
* @return StoreInterface
* @throws GraphQlInputException
*/
public function execute(array $storeData): StoreInterface
{
try {
$this->validateData($storeData);
$storeObject = $this->createStoreObject($storeData);
$this->storeRepository->save($storeObject);
} catch (LocalizedException $e) {
// Re-throw as GraphQL input exception for proper API error handling
throw new GraphQlInputException(__($e->getMessage()), $e);
}
return $storeObject;
}
/**
* Validates required fields. A real implementation would be more thorough.
*
* @param array $data
* @throws LocalizedException
*/
private function validateData(array $data): void
{
if (empty($data['name'])) {
throw new LocalizedException(__('The store name cannot be empty.'));
}
}
/**
* Populates a StoreInterface DTO using the factory and DataObjectHelper.
*
* @param array $data
* @return StoreInterface
*/
private function createStoreObject(array $data): StoreInterface
{
/** @var StoreInterface $store */
$store = $this->storeFactory->create();
$this->dataObjectHelper->populateWithArray(
$store,
$data,
StoreInterface::class
);
return $store;
}
}
The service uses Magento's repository and factory patterns. DataObjectHelper efficiently maps the raw array to the typed Data Interface. Validation is minimal here, just checking for a name. You'd want to add more checks, maybe for coordinate ranges or postal code format.
Defining the Mutation in the Schema
The GraphQL schema file is where you wire your PHP logic to the API structure. Mutations are defined alongside queries. You must update your schema.graphqls file.
Add this mutation definition to your existing schema.graphqls:
type Mutation {
createPickupStore(input: PickupStoreInput!): PickupStoreOutput
@resolver(class: "Vendor\\GraphQLStorePickup\\Model\\Resolver\\CreatePickUpStore")
@doc(description: "Creates a new in-store pickup location.")
}
type PickupStoreOutput {
pickup_store: PickupStore
}
input PickupStoreInput {
name: String!
street: String
street_number: Int
city: String
postcode: String
latitude: Float
longitude: Float
}
Key points: The ! denotes a non-nullable field. The @resolver directive points to our class. The output type mirrors the query's PickupStore type to ensure consistency.
Module Installation and Deployment
Enable the module and apply schema changes. Since we are modifying the DB schema (assuming the initial module created a table), you must generate a whitelist and upgrade.
bin/magento module:enable Vendor_GraphQLStorePickup
bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_GraphQLStorePickup
bin/magento setup:upgrade
The whitelist command is critical for Magento 2.4 and above when using declarative schema. It prevents the system from removing your custom table.
Testing the Mutation
You can't just hit the /graphql endpoint in a browser. Use a GraphQL client like Altair, Postman with GraphQL support, or the integrated GraphiQL explorer if you have a module for it. Here's a sample mutation payload.
mutation {
createPickupStore(
input: {
name: "Downtown Flagship"
street: "Commerce Ave"
street_number: 455
city: "Brooklyn"
postcode: "11201"
latitude: 40.6782
longitude: -73.9442
}
) {
pickup_store {
entity_id
name
city
}
}
}
A successful response will return the fields you requested, confirming the record was created with its new entity_id. If you mess up a required field, the GraphQL layer will return a structured error pointing to the input argument.
Building this forces you to consider Magento's layered architecture. Maybe that's the point. You separate routing, validation, and business logic. The result is a mutation that's clear, maintainable, and ready for you to add authentication and complex validation. This approach scales.