docs/apis/dual-api/architecture.md
This document explains how the pieces fit together. For how to actually write classes, see Extending the code API.
The dual API has two halves:
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.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.
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.
Two conventions drive most behavior:
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.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 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.
| Path | Contents | Edit? |
|---|---|---|
src/Api/ | The code API: attributes, queries, mutations, types, input types, enums, interfaces, scalars, pagination, utils | Yes, 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 definitions | No, 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 builds | Rarely; 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.
When a GraphQL request hits the endpoint, the controller (a generated subclass of GraphQLControllerBase):
PrincipalResolver.ClassResolver, check authorization, and call execute().HttpStatusResolver).Each of these steps is a documented extension point; see Authentication and authorization, Settings and caching, and Infrastructure classes.
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.