Skip to main content

Dual API architecture

This document explains how the pieces fit together. For how to actually write classes, see Extending the code API.

Two halves, one source

The dual API has two halves:

  • The code API: plain PHP classes under src/Api/. They are GraphQL-agnostic: they import nothing from any GraphQL library and work as a standalone, in-process PHP API. This is the authoritative, manually maintained source.
  • The autogenerated GraphQL layer: code under src/Internal/Api/Autogenerated/ produced by a build script from the code API. It is committed to source control but never hand-edited. It powers a GraphQL endpoint (by default POST|GET /wp-json/wc/graphql).

The build script reads the code API and (re)generates the GraphQL layer. The relationship is one-directional: you change PHP classes, then regenerate.

src/Api/ ──(build:api)──▶ src/Internal/Api/Autogenerated/ ──▶ /wp-json/wc/graphql
(you edit this) (generated, committed, never edited) (GraphQL endpoint)

Because the generated tree is committed to source code, regenerating it after a source change is mandatory; a staleness check enforces this in GitHub's CI pipeline for pull requests.

Code-first and the command pattern

The code API is organized around the command pattern: each query or mutation is a class with a single execute() method (plus an optional authorize() method). Output types, input types, enums, interfaces, and scalars are likewise plain classes/enums.

#[Name( 'product' )]
#[Description( 'Retrieve a single product by ID.' )]
#[RequiredCapability( 'read_product' )]
class GetProduct {
#[ReturnType( Product::class )]
public function execute( int $id ): ?object {
// ...
}
}

The build script infers as much as it can from code structure and uses PHP 8 attributes only where structure is not enough.

Convention over configuration

Two conventions drive most behavior:

  • Directory placement determines role. A class in Queries/ becomes a GraphQL query; one in Types/ becomes an output type; one in Enums/ becomes an enum; and so on. Arbitrary nested subdirectories are allowed for organization (e.g. Queries/Coupons/GetCoupon.php) - nesting does not change the role. See Recognized directories.
  • Names are derived, then overridable. GraphQL type names default to the PHP class name; query/mutation names to its camelCase form; fields to property names as-is; enum values from PascalCase to SCREAMING_SNAKE_CASE. Any of these can be overridden with #[Name( '...' )].

Attributes fill the gaps that conventions cannot: descriptions, authorization, type shaping (arrays, connections, custom scalars), deprecation, and metadata. See the Attributes reference.

The GraphQL engine is an implementation detail

The GraphQL endpoint is currently powered by the webonyx/graphql-php package, vendored and re-namespaced to Automattic\WooCommerce\Vendor\GraphQL\* to avoid version conflicts with other plugins.

This is deliberately hidden from code-API authors. The autogenerated code never references Vendor\GraphQL\* directly: it references only a thin, WooCommerce-owned schema surface under Api\Infrastructure\Schema\*. That surface is the single point of contact with the engine, so the engine could be replaced in the future without breaking already-committed generated code in plugins. As a code-API author you never see GraphQL types at all; as an infrastructure maintainer, see Extending the infrastructure.

Where things live

PathContentsEdit?
src/Api/The code API: attributes, queries, mutations, types, input types, enums, interfaces, scalars, pagination, utilsYes, this is the source
src/Api/Infrastructure/Public, engine-decoupled runtime surface and convention classes (Principal, ClassResolver, GraphQLControllerBase, the Schema\* wrappers, ...)Rarely; infrastructure only
src/Internal/Api/Autogenerated/Generated GraphQL resolvers and type definitionsNo, regenerate instead
src/Internal/Api/Internal runtime not referenced by external code (QueryCache, Settings, endpoint registrar, query rules)Rarely; core only
bin/api-builder/The build tooling (ApiBuilder, build-api.php, staleness checker, templates). Not shipped in release buildsRarely; infrastructure only

The generated tree mirrors the role directories: Autogenerated/GraphQLQueries/, GraphQLMutations/, and GraphQLTypes/{Output,Input,Enums,Interfaces,Scalars,Pagination}/, plus a RootQueryType, RootMutationType, and TypeRegistry.

Request lifecycle (summarized)

When a GraphQL request hits the endpoint, the controller (a generated subclass of GraphQLControllerBase):

  1. Resolves a principal for the request (who is calling) via the configured PrincipalResolver.
  2. Parses and validates the query (depth and complexity limits; optional caching of the parsed AST).
  3. Runs the resolvers, which look up the corresponding command class through the ClassResolver, check authorization, and call execute().
  4. Formats the result (or errors) and picks an HTTP status code (optionally via a plugin-supplied HttpStatusResolver).

Each of these steps is a documented extension point; see Authentication and authorization, Settings and caching, and Infrastructure classes.

Reusable by plugins

Everything above applies to a plugin that wants its own dual API. A plugin defines its own src/Api/ tree, runs the same builder against it, commits the generated output to its own repo, and registers a dedicated GraphQL endpoint. It reuses WooCommerce's infrastructure and can supply its own convention classes (authentication, class resolution, status codes) and attributes where it needs to diverge from the defaults. See Creating a dual API in a plugin.