Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Magento GraphQL Architecture Overview: schema.graphqls, Resolvers, DataProviders

Magento GraphQL Architecture Overview: schema.graphqls, Resolvers, DataProviders

~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Before writing any custom code, it's worth looking at the three building blocks every piece of Magento GraphQL functionality is made of: the schema declaration in schema.graphqls, the resolver, and - optional but customary - a separate DataProvider class. Once you understand these three building blocks and how they interact, you can read and extend any Magento GraphQL functionality, no matter how deeply it's nested in core.

schema.graphqls: the declaration

Any GraphQL-capable module can ship a file etc/schema.graphqls. It's written in the GraphQL Schema Definition Language (SDL) - no PHP, no XML, but the syntax mandated by the GraphQL specification itself. On startup, Magento collects all schema.graphqls files from every active module and merges them into one single, global schema - which is why a module can use extend type to attach fields to types defined in a completely different module (block 3).

app/code/Magento/CatalogGraphQl/etc/schema.graphqls (excerpt, shortened)
type Query {
    products(
        search: String
        filter: ProductAttributeFilterInput
        pageSize: Int = 20
        currentPage: Int = 1
        sort: ProductAttributeSortInput
    ): Products
        @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products")
        @doc(description: "The products query searches for products")
}

The decisive line is @resolver(class: "...") - a directive that ties a query or mutation field to a PHP class. Without this directive, Magento knows the field's type but has no idea which code actually supplies the data for it.

A resolver is a plain PHP class implementing \Magento\Framework\GraphQl\Query\ResolverInterface. For every field carrying a @resolver directive, Magento calls its resolve() method at runtime:

interface ResolverInterface
{
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        ?array $value = null,
        ?array $args = null
    );
}

$args holds the arguments passed by the client (e.g. search, pageSize), $context carries information about the calling customer/store/website, and the return value is serialized directly into the JSON response. Chapter 10 covers each of these parameters in detail.

The DataProvider: clean separation of resolution and data access

Magento itself deliberately keeps resolvers thin and pushes the actual data fetching into a separate DataProvider class - not an interface enforced by the framework, but a convention found in practically every core GraphQl module. The resolver stays lean (accept arguments, call the DataProvider, shape the result), while the DataProvider handles the actual business logic - typically by reusing an existing repository instead of touching collections directly.

Typical layout in a core GraphQl module

Magento/CatalogGraphQl/
├── etc/
│   └── schema.graphqls
└── Model/
    └── Resolver/
        ├── Products.php            (implements ResolverInterface)
        └── Products/
            └── DataProvider/
                └── Product.php      (builds SearchCriteria, calls repository)

The full lifecycle of a request

  1. The client sends a GraphQL query via POST to /graphql.
  2. Magento's GraphQL controller (Magento\GraphQl\Controller\GraphQl) parses the query against the merged, global schema.
  3. For each requested field carrying a @resolver directive, the matching resolver class is instantiated and resolve() is called.
  4. The resolver delegates to a DataProvider, which - usually via a repository or a collection - loads the data from the database.
  5. The resolver's return value (an associative array) is validated against the type declared in the schema and serialized to JSON.

Tipp: Fields without their own @resolver directive aren't a mistake: Magento resolves them automatically via a default resolver that simply reads the same-named key from the array returned by the parent query. A custom resolver only pays off once a field needs its own logic - more on that in chapter 5.

Chapter 3 uses exactly this existing lifecycle: querying the built-in GraphQL interface with a real client, without writing a single line of PHP yet.