The GraphQL API for WordPress is a standard GraphQL server, satisfying the behavior defined by the GraphQL spec.

But in addition, it offers a set of special features, destined to help you make the most out of GraphQL. These are described below.

Already integrated to WordPress permalink

The plugin is directly integrated to WordPress, enabling to fetch any piece of data:

  • Posts
  • Custom Posts
  • Pages
  • Users
  • User Roles
  • Comments
  • Tags
  • Media
  • Blocks

Interactive clients permalink

The plugin offers user-friendly tools to interact with the GraphQL service.

Executing the GraphQL queries is done with GraphiQL:

Executing a query via GraphiQL
Executing a query via GraphiQL

Interactively visualizing the GraphQL schema is done with GraphQL Voyager:

Interacting with the schema via GraphQL Voyager
Interacting with the schema via GraphQL Voyager

Multiple layers of security permalink

Several mechanisms have been put in place to help protect the data:

πŸ‘‰πŸ½ We can expose pre-defined data through persisted queries, and avoid granting public access to the single endpoint.

πŸ‘‰πŸ½ We can create custom endpoints, setting permissions for every field in the schema through access control lists, and exposing their schema through their own public clients.

πŸ‘‰πŸ½ We can define the API to be either public or private.

πŸ‘‰πŸ½ The single endpoint, and clients to interact with it, are disabled by default.

πŸ‘‰πŸ½ The admin can grant user-role access to editing the schema.

This image shows an interactive schema client, which must be enabled to grant public access to it:

Public Voyager client, disabled by default
Public Voyager client, disabled by default

Custom endpoints permalink

A GraphQL server normally exposes a single endpoint for retrieving and posting data, providing a unique behavior for all users and applications.

With custom endpoints, we can create and expose a custom GraphQL schema under its own URL, and grant access to this endpoint to a specific target:

πŸ‘‰πŸ½ Some client or user
πŸ‘‰πŸ½ A group of users with more access to features (such as PRO users)
πŸ‘‰πŸ½ One of the several applications, like mobile app or website
πŸ‘‰πŸ½ 3rd-party APIs
πŸ‘‰πŸ½ Any other

This video demonstrates publishing a custom endpoint:

Persisted queries permalink

In a REST API, we create multiple endpoints, each returning a pre-defined set of data. In a GraphQL API, in contrast, we provide any query to a single endpoint, which returns exactly the requested data.

Persisted queries are normal GraphQL queries, however they are stored in the server and accessed under their own URL, thus emulating a REST endpoint. They provide the advantages from these two APIs, while avoiding their disadvantages:

βœ… Accessed via GET or POST❌ Accessed only via POST
βœ… Can be cached on the server or CDN❌ Needs to provide an extra layer in client-side just for caching
βœ… It's secure: only intended data is exposed❌ Data is exposed to anyone, including malicious actors
βœ… No under/over fetching of data, all data is retrieved in a single request❌ It can be slow, since the application may need several requests to retrieve all the data
βœ… It enables rapid iteration of the project❌ It's tedious to create all the endpoints
βœ… It can be self-documented❌ Producing documentation is mandatory
βœ… It provides clients to create and publish the query❌ Publishing endpoints is done via code

This video demonstrates creating a persisted query:

Access control lists permalink

The GraphQL endpoint, which can return any piece of data accessible through the schema, could potentially allow malicious actors to retrieve private information. Hence, we must implement security measures to protect the data.

With access control lists, we can define who can access each field and directive in the schema:

πŸ‘‰πŸ½ Disable access to everyone
πŸ‘‰πŸ½ Grant access if the user is logged-in, or logged-out
πŸ‘‰πŸ½ Grant access if the user has some role
πŸ‘‰πŸ½ Grant access if the user has some capability

This video demonstrates creating permissions for different fields in the schema:

Public/private API mode permalink

What should happen when a user without access to some field or directive in the schema, attempts to access it?

With the public/private API mode we can control the desired behavior:

In the public API, the fields in the schema are exposed, and when the permission is not satisfied, the user gets an error message with a description of why the permission was rejected.

In the private API, the schema is customized to every user, containing only the fields available to him or her, and so when attempting to access a forbidden field, the error message says that the field doesn't exist.

This video demonstrates making an API either public or private:

HTTP caching permalink

Because it sends the queries via POST, GraphQL is normally not cacheable on the server-side or intermediate stages between the client and the server (such as a CDN), and we need to worry about adding a caching layer on the application on the client-side, making it slower and more complex.

However, because persisted queries can be accessed via GET, their response can be cached through standard HTTP caching. We can define for how long every field or directive must be cached, and the response will include a Cache-Control header, whose max-age value is calculated automatically from all the fields and directives in the requested query (or no-store if it involves user state).

This video demonstrates how the API response makes use of HTTP caching:

API hierarchy permalink

An API may expose several endpoints which are somehow related to each other, and which may execute a similar query. This is the case, for instance, when creating endpoints that exposes the data in one language or another.

Through the API Hierarchy we can define a structure for endpoints, so that we can produce:

  • /graphql/posts/english/
  • /graphql/posts/french/

In this case, a parent query posts can provide the GraphQL query, and its descendant queries english and french provide the variables to customize the query.

This video demonstrates creating an API hierarchy:

Field deprecation permalink

GraphQL does not version an endpoint, as it happens in REST. Instead, it makes the schema evolve, by allowing to deprecate fields.

Deprecation is standard GraphQL behavior, but is normally executed via code. Through the Field deprecation user interface, we can already deprecate fields, without the need to deploy any code.

This video demonstrates how to deprecate a field:

Multiple query execution permalink

Query batching enables the GraphQL server to execute multiple queries in a single request, but those queries are merely executed one after the other, independently from each other.

Multiple query execution is an improvement over query batching, by combining all queries together and executing them as a single operation, reusing their state and their data. For instance, if a first query fetches some data, and a second query also accesses the same data, this data is retrieved only once, not twice.

This feature improves performance, for whenever we need to execute an operation against the GraphQL server, then wait for its response, and then use that result to perform another operation. By combining them together, we are avoiding the latency from the extra request(s).

This gif shows how multiple queries are executed together:

Executing multiple queries in a single operation
Executing multiple queries in a single operation

Schema namespacing permalink

If plugins WooCommerce and Easy Digital Downloads both implemented a Product type for the GraphQL API, then we could not install both plugins at the same time, since their types would conflict.

Schema namespacing enables to avoid conflicts in the schema, by having all type names namespaced. Hence, type Product would become Automattic_WooCommerce_Product and SandhillsDev_EasyDigitalDownloads_Product respectively, and these types could be added to the same schema.

This images shows a namespaced schema (where type Post is namespaced as PoPSchema_Posts_Post, and others):

Namespaced schema
Namespaced schema

Nested mutations permalink

Mutations are only exposed in the root type in GraphQL. As a consequence, the root type becomes heavily bloated, containing fields with nothing in common among themselves other than being mutations (which is a technical matter, not an interface design decision).

Nested mutations make the schema more logical and browsable, by enabling to perform mutations on any type, and not only on the root type. They help performance too, by allowing to modify data on the result from another mutation, thus avoiding the latency from executing multiple requests.

This GraphQL query demonstrates a nested mutation:

mutation {
createPost(title: "First title") {
update(title: "Second title", content: "Some content") {
addComment(comment: "My first comment") {

Embeddable fields permalink

Fields in GraphQL are not composable, hence we can't pass the result of a field as an argument to another field.

Embeddable fields fill this void. It enables to query a field, passing as argument the value of another field through the mustache syntax {{ field }}. This way, fields become composable, while still abiding by the GraphQL spec.

This query injects the value from several fields when composing a newsletter:

mutation {
to: "{{ newsletterRecipients }}",
subject: "My latest post: {{ postTitle(id: 5) }}",
content: "On {{ postDate(id: 5) }} I published my latest post: {{ postContent(id: 5) }}"

Composable directives permalink

Oftentimes, a directive cannot be applied on a field, because it has an input which is different than the field's output. For instance, directive @upperCase receives a string as input, so it can't be applied on field User.capabilities, which returns an array of strings.

With composable directives, a directive can augment another directive to modify its behavior or fill a gap. This removes the need to duplicate fields or directives just to change their input or return types, avoiding bloat.

In this query, directive @forEach iterates over an array of strings, and applies its nested directive @upperCase on each of them, fixing the type mismatch:

users {
@upperCase(nestedUnder: -1)