GraphQL
GraphQL is the second kind of API in Magento. Unlike REST, there is a single URL — /graphql — and the client lists the fields it needs in the query and gets exactly those. GraphQL is used by JavaScript storefronts and mobile apps. A module adds its queries to the shared schema and writes a class that returns the data for them — a resolver.
Where the files live
| File | What is in it |
|---|---|
etc/schema.graphqls | The definition of queries and types |
Model/Resolver/Reviews.php | The resolver — a class that returns data |
Both files are created by hand.
app/code/Acme/Hello/
├── Model/
│ └── Resolver/
│ └── Reviews.php new file
└── etc/
├── module.xml changed
└── schema.graphqls new fileIn module.xml add the Magento_GraphQl module to the sequence section and run bin/magento setup:upgrade.
The schema
# app/code/Acme/Hello/etc/schema.graphqls
type Query {
helloReviews(productId: Int!): [HelloReview] @resolver(class: "Acme\\Hello\\Model\\Resolver\\Reviews")
}
type HelloReview {
author: String
rating: Int
}The file adds the helloReviews query with a required productId argument to the shared Query type. The query returns a list of HelloReview objects with two fields. The @resolver directive names the class that returns the data; the backslash in the class name is doubled.
The resolver
// app/code/Acme/Hello/Model/Resolver/Reviews.php
namespace Acme\Hello\Model\Resolver;
use Acme\Hello\Api\ReviewRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
class Reviews implements ResolverInterface
{
public function __construct(
private ReviewRepositoryInterface $reviewRepository,
private SearchCriteriaBuilder $criteriaBuilder,
) {}
public function resolve(Field $field, $context, ResolveInfo $info, ?array $value = null, ?array $args = null)
{
$criteria = $this->criteriaBuilder
->addFilter('product_id', (int) $args['productId'])
->create();
$reviews = [];
foreach ($this->reviewRepository->getList($criteria)->getItems() as $review) {
$reviews[] = [
'author' => $review->getAuthor(),
'rating' => $review->getRating(),
];
}
return $reviews;
}
}Query arguments arrive in the $args array. The resolver finds reviews through the repository and returns an array whose keys match the field names of the HelloReview type.
Checking
bin/magento cache:clean
curl -X POST http://shop.test/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ helloReviews(productId: 1) { author rating } }"}'The server answers with a JSON object where data.helloReviews holds the list of authors and ratings of the reviews for product 1. If you remove rating from the query, only authors remain in the response.
The query itself, without the wrapper, looks like this:
{
helloReviews(productId: 1) {
author
rating
}
}It is more convenient to send queries from a dedicated GraphQL client: it reads the shop schema and suggests fields.
Queries and mutations
| Type in the schema | What it is for |
|---|---|
Query | Reading data |
Mutation | Changing: create, update, delete |
A mutation is described the same way — as a field in the Mutation type with the @resolver directive. Its data is usually passed as a single argument of an input type declared with the input keyword.
Errors
If a query cannot be executed, the resolver throws an exception from the Magento\Framework\GraphQl\Exception namespace, for example GraphQlInputException for wrong arguments. The client gets the message in the errors section of the response.
Common mistake
The schema was changed, but the server answers that the helloReviews field does not exist. The merged schema is cached: run bin/magento cache:clean after every edit of schema.graphqls. The second cause is a class name in @resolver written with one backslash instead of two.