
How to Enable and Use WPGraphQL in WordPress?
WPGraphQL is a free plugin that adds a GraphQL API to WordPress, letting a front end request exactly the fields it needs, across posts, media, authors, and taxonomies at once, in a single query instead of several separate REST calls. It's the most widely used GraphQL implementation for WordPress and a common alternative (or companion) to the WordPress REST API in headless setups.
This guide covers installing WPGraphQL, exploring its schema with GraphiQL, writing queries that pull related data in one request, filtering and paginating results, and calling it from a Next.js front end.
Installing WPGraphQL
WPGraphQL is a standard WordPress plugin, install it the normal way:
- In the WordPress admin, go to Plugins → Add New Plugin.
- Search for "WPGraphQL," find the plugin by WPGraphQL, and click Install Now, then Activate.
- A new GraphQL menu item appears in the admin sidebar, along with a single endpoint at
/graphql(for example,https://example.com/graphql).
Unlike the REST API, which exposes a separate URL per resource type (/wp-json/wp/v2/posts, /wp-json/wp/v2/media, and so on), WPGraphQL exposes exactly one endpoint. Every query, no matter what data it asks for, is a POST request to that same /graphql URL.
Exploring the Schema with GraphiQL
WPGraphQL ships with an in-admin GraphiQL IDE (GraphQL → GraphiQL IDE) for writing and testing queries against your actual site data before you write any front-end code. It shows the full schema on the right-hand side, every type, field, and argument WPGraphQL exposes, and autocompletes as you type, which is the fastest way to discover what's queryable without reading documentation first.
Your First Query: Fetching Posts
A basic query for a list of posts, their titles, and their excerpts:
query GetPosts {
posts(first: 10) {
nodes {
id
title
slug
excerpt
date
}
}
}
Run this in GraphiQL, or send it as a POST request from any client:
const query = `
query GetPosts {
posts(first: 10) {
nodes {
id
title
slug
excerpt
date
}
}
}
`;
const res = await fetch("https://example.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const { data } = await res.json();
console.log(data.posts.nodes);
Notice the response shape mirrors the query's shape exactly, data.posts.nodes is an array of objects containing only the fields you asked for, nothing more. This is the core advantage over REST: no over-fetching a large, fixed response shape when you only need three fields.
Fetching Related Data in One Request
This is where WPGraphQL earns its keep. A single query can pull a post's featured image, author, and categories, data that would take several REST requests (or the _embed parameter, with a fixed shape) to assemble:
query GetPostWithRelations($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
date
featuredImage {
node {
sourceUrl
altText
}
}
author {
node {
name
avatar {
url
}
}
}
categories {
nodes {
name
slug
}
}
}
}
Variables ($slug here) are passed alongside the query rather than interpolated into the query string:
const query = `
query GetPostWithRelations($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
featuredImage {
node {
sourceUrl
altText
}
}
}
}
`;
const res = await fetch("https://example.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
variables: { slug: "hello-world" },
}),
});
const { data } = await res.json();
Filtering, Sorting, and Paginating
WPGraphQL's where argument handles filtering, similar to REST's query parameters but nested inside the query itself:
query PostsByCategory {
posts(first: 5, where: { categoryName: "wordpress", orderby: { field: DATE, order: DESC } }) {
nodes {
title
slug
}
}
}
Pagination uses cursor-based pagination (the Relay connection spec) rather than page numbers, which handles content changing between requests more reliably than offset-based pagination:
query PaginatedPosts($after: String) {
posts(first: 10, after: $after) {
pageInfo {
hasNextPage
endCursor
}
nodes {
title
slug
}
}
}
Fetch the next page by passing the previous response's pageInfo.endCursor back in as the after variable.
Using WPGraphQL from a Next.js Front End
A minimal Next.js server component fetching a post through WPGraphQL:
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch("https://cms.example.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query GetPost($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
featuredImage {
node { sourceUrl altText }
}
}
}
`,
variables: { slug },
}),
next: { revalidate: 3600 },
});
const { data } = await res.json();
return data.post;
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
{post.featuredImage && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={post.featuredImage.node.sourceUrl}
alt={post.featuredImage.node.altText ?? ""}
/>
)}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
This is largely the same shape as the REST-based Next.js integration, just a single POST request to /graphql instead of one or more GET requests to REST endpoints.
Writing a Mutation
Reading data covers most headless front-end needs, but WPGraphQL also supports mutations for creating and updating content, gated behind authentication exactly like a REST API write. A mutation creating a draft post looks like this:
mutation CreateDraftPost($title: String!, $content: String!) {
createPost(input: { title: $title, content: $content, status: DRAFT }) {
post {
id
slug
status
}
}
}
Sent the same way as a query, just with a different query string value and, critically, an Authorization header carrying a valid token:
const res = await fetch("https://example.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
body: JSON.stringify({
query: `
mutation CreateDraftPost($title: String!, $content: String!) {
createPost(input: { title: $title, content: $content, status: DRAFT }) {
post { id slug status }
}
}
`,
variables: { title: "Draft from the API", content: "<p>Body text.</p>" },
}),
});
Handling GraphQL Errors
Unlike a REST API, WPGraphQL almost always responds with a 200 status code, even when something went wrong, errors are reported inside the response body's errors array rather than through the HTTP status:
{
"data": { "post": null },
"errors": [
{
"message": "No post exists with id: bad-slug",
"extensions": { "category": "user" }
}
]
}
Always check for an errors key in the parsed response before assuming data is complete or correct, a partial response can include both real data and errors simultaneously, for example, when one field in a larger query fails to resolve but the rest of the query succeeds.
Using a Typed Client Instead of Raw fetch()
The raw fetch() calls above work fine, but larger projects often reach for a small GraphQL client to handle request boilerplate, TypeScript typing, and caching. graphql-request is a common lightweight choice:
import { GraphQLClient, gql } from "graphql-request";
const client = new GraphQLClient("https://example.com/graphql");
const query = gql`
query GetPost($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
}
}
`;
const data = await client.request(query, { slug: "hello-world" });
For projects already using Apollo Client or urql on the front end, WPGraphQL works as a standard GraphQL endpoint with either, no WordPress-specific client library is required, since WPGraphQL implements the standard GraphQL specification rather than a WordPress-proprietary variant.
Extending the Schema for Custom Fields
If you use Advanced Custom Fields, the companion WPGraphQL for ACF plugin automatically adds your ACF field groups to the GraphQL schema, no manual field registration needed. For fields that don't come from ACF, you can register a field directly in PHP:
add_action( 'graphql_register_types', function () {
register_graphql_field( 'Post', 'readingTime', [
'type' => 'Int',
'description' => 'Estimated reading time in minutes',
'resolve' => function ( $post ) {
$word_count = str_word_count( wp_strip_all_tags( $post->contentRaw ) );
return (int) ceil( $word_count / 200 );
},
] );
} );
After this, readingTime becomes a queryable field on the Post type, alongside every built-in field, resolvable in the exact same query as everything else.
Custom Post Types and WPGraphQL
A custom post type only appears in the GraphQL schema if it was registered with show_in_graphql set to true, along with graphql_single_name and graphql_plural_name:
register_post_type( 'project', [
'public' => true,
'show_in_rest' => true,
'show_in_graphql' => true,
'graphql_single_name' => 'project',
'graphql_plural_name' => 'projects',
// ...other args
] );
Once registered this way, project and projects become queryable fields on the root Query type, following the same nodes/pageInfo shape as the built-in posts field.
Debugging Schema Changes
After registering a custom field or exposing a new post type, the change doesn't always show up immediately in GraphiQL's autocomplete due to WPGraphQL's internal schema caching. Two things worth checking when a field seems to be missing:
- Clear the WPGraphQL schema cache under GraphQL → Settings, or programmatically via the
graphql_flush_schemaaction, after registering new types or fields in PHP. - Confirm
graphql_register_types(or the equivalent registration hook) actually ran. A common mistake is callingregister_graphql_field()too early, before WPGraphQL has initialized its own type registry, which silently does nothing rather than throwing a visible error.
Frequently Asked Questions (FAQ) About WPGraphQL
No, they run independently and don't conflict. Many sites use both: WPGraphQL for the front end's main content queries, and the REST API for specific integrations (webhooks, third-party plugins) that already expect it.
No, it's a third-party plugin, though a very widely adopted one in the headless WordPress community, with an active maintainer team and a large plugin ecosystem of its own (WPGraphQL for ACF, WPGraphQL for WooCommerce, and others).
Yes, through GraphQL mutations, which follow the same authenticated-request model as REST API writes. Most setups use JSON Web Tokens for mutation authentication, via a companion plugin like WPGraphQL JWT Authentication.
It needs show_in_graphql set to true at registration, along with graphql_single_name and graphql_plural_name. show_in_rest being true doesn't automatically expose it to GraphQL; the two are configured independently.
Yes, for authenticated requests with sufficient permissions, following the same rule as the REST API: unauthenticated requests only ever see published, public content, regardless of what the query asks for.
Not automatically, both ultimately run the same underlying WordPress database queries. The performance win comes from making fewer round trips (one GraphQL request instead of several REST requests) and from not transferring fields you don't need, which matters most on pages that combine many related pieces of data.
WPGraphQL exposes registered nav menus and their items as a menuItems field out of the box, which is generally simpler than the custom endpoint work required to expose menus over the REST API.
Conclusion
WPGraphQL turns WordPress into a single, flexible GraphQL endpoint where a front end asks for exactly the fields it needs, across posts, media, authors, and custom fields, in one request. Installing it is a standard plugin install, and GraphiQL gives you a way to explore and test the full schema against your real content before writing any front-end code.
The tradeoff against the REST API is mostly about shape and round trips, GraphQL avoids over-fetching and lets you assemble related data in one request, while REST's fixed endpoints are simpler to reason about for straightforward cases. Many headless WordPress projects use both side by side, GraphQL for the front end's main queries, REST for specific integrations that expect it. If you haven't already, it's worth comparing this directly against the REST API for your specific use case before committing to one.
A few additional resources if you want to go deeper:
- WPGraphQL Documentation — the official documentation, including the full schema reference.
- WPGraphQL for ACF — the companion plugin exposing Advanced Custom Fields data to the schema.
- WordPress Developer Reference: register_post_type() — details on the show_in_graphql and related arguments used above.


