In a headless WordPress setup, the choice between the WP REST API and the WPGraphQL plugin shapes both your velocity and your long-term maintenance.
I’ve used REST on two projects and GraphQL on one. Here are the similarities, the differences, and the criteria I use to pick.
Where the WP REST API stands
In core since WP 4.7. No setup required. Endpoints:
GET /wp-json/wp/v2/posts– post listGET /wp-json/wp/v2/posts/{id}– single postGET /wp-json/wp/v2/pages– pagesGET /wp-json/wp/v2/categories– categoriesGET /wp-json/wp/v2/users/{id}– userPOST /wp-json/wp/v2/posts– create post (auth required)
For a custom post type:
register_post_type('product', [
'show_in_rest' => true,
'rest_base' => 'products'
]);Endpoint: /wp-json/wp/v2/products
REST API strengths
Built-in, zero setup. No plugin, nothing to install. Shipped in every WP version.
RESTful patterns. CRUD is URL-driven. Caching is HTTP-native (ETag, Cache-Control).
Authentication flexibility. Application passwords, JWT plugin, or OAuth plugin.
Consumer tooling. Every framework speaks REST. Native in Postman.
Low learning curve. Every developer knows REST.
REST API limitations
Over-fetching. Listing posts pulls every field, with no clean way to say “just title and URL”. The _fields parameter helps but is limited.
Multiple requests. Post + author + category means three endpoint calls. _embed can embed author or featured_media, but it’s not enough for complex nested queries.
Custom field exposure. ACF fields aren’t exposed by default. Each one needs register_rest_field boilerplate.
Filtering is limited. Meta queries aren’t direct in REST; you have to register custom filters.
The WPGraphQL plugin
WPGraphQL is a plugin that adds a GraphQL endpoint to WordPress. It maps the WP schema onto GraphQL types.
Setup:
- Install the plugin (from wordpress.org or via composer)
- Activate
- Endpoint:
/graphqlis available immediately - GraphiQL lives in the admin (for debugging)
Sample query:
query GetPost($slug: String!) {
postBy(slug: $slug) {
title
content
author {
node {
name
avatar {
url
}
}
}
categories {
nodes {
name
slug
}
}
featuredImage {
node {
sourceUrl
altText
}
}
}
}One request returns post + author + categories + featured image. REST would need four.
GraphQL strengths
Single request, multi-resource. Nested queries collapse into one HTTP round trip.
Field selection. The client picks its fields, the server doesn’t send anything else. Payload stays small.
Type safety. Schema drives TypeScript (or any language) type generation. IDE autocompletion is excellent.
ACF support. With WPGraphQL for ACF, every ACF field lands in the GraphQL schema.
Subscriptions. Real-time updates are possible (rare, but available).
Developer experience. GraphiQL for schema exploration and query testing.
GraphQL pain points
Plugin dependency. WPGraphQL is a third-party plugin, not core. Community-maintained, no commercial support. If the maintainer steps away, you’ve got risk.
Learning curve. A team new to GraphQL has extra to absorb.
Caching is messier. HTTP cache doesn’t play well with a single endpoint and POST requests. CDN caching is hard. You need application-level caching (Apollo, urql).
Schema maintenance. New custom fields mean registering GraphQL types. WPGraphQL doesn’t expose them automatically.
Error handling. GraphQL errors come back inside 200 OK responses (in an errors array). Status-code-based error handling doesn’t work.
Performance comparison
Testing against the same dataset:
Scenario: blog post + 3 related posts + author + category.
REST API:
– 5 requests
– Total transfer: 48 KB
– Round-trip latency (3G): 1.8 seconds
GraphQL:
– 1 request
– Total transfer: 12 KB (with field selection)
– Round-trip latency (3G): 450 ms
On mobile or slow networks GraphQL is a clear win.
On desktop / fast networks the gap narrows.
Implementation in Next.js
With REST:
export async function getStaticProps({ params }) {
const [post, categories, author] = await Promise.all([
fetch(`${WP_URL}/wp-json/wp/v2/posts?slug=${params.slug}`).then(r => r.json()),
fetch(`${WP_URL}/wp-json/wp/v2/categories`).then(r => r.json()),
fetch(`${WP_URL}/wp-json/wp/v2/users/${post.author}`).then(r => r.json())
]);
return { props: { post: post[0], categories, author } };
}With GraphQL:
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient(`${WP_URL}/graphql`);
export async function getStaticProps({ params }) {
const query = `
query($slug: String!) {
postBy(slug: $slug) {
title
content
author { node { name } }
categories { nodes { name slug } }
}
}
`;
const data = await client.request(query, { slug: params.slug });
return { props: { post: data.postBy } };
}GraphQL is more compact, and the type-generation payoff lands on the frontend.
TypeScript + codegen
WPGraphQL plus GraphQL Code Generator auto-generates TS types:
npx graphql-codegen --config codegen.ymlcodegen.yml:
schema: https://your-wp.com/graphql
documents: './src/**/*.graphql'
generates:
./src/generated/types.ts:
plugins:
- typescript
- typescript-operationsOn the frontend:
import { GetPostQuery } from './generated/types';
const data: GetPostQuery = await client.request(query);
console.log(data.postBy.title); // fully typedWith REST you end up writing types by hand or defining an OpenAPI spec.
When to pick which
Pick REST when:
- Small project, fast setup
- Team isn’t used to GraphQL
- Public API for third-party consumers (broader tooling)
- Cache-heavy use cases (HTTP cache)
- You don’t want the WPGraphQL plugin dependency
Pick GraphQL when:
- Mobile-first, network-sensitive
- Complex nested data queries
- TypeScript frontend
- Heavy use of custom fields (ACF)
- Developer experience is a priority
Authentication
Two authentication cases in headless setups:
Read-only public content: no auth. Posts, pages, products are publicly readable.
Write operations (comments, form submissions, user data): auth required.
REST API:
– Application passwords (core feature since WP 5.6)
– JWT plugin
– OAuth2 plugin
GraphQL:
– WPGraphQL supports the same auth methods
– Credentials in the request header
JWT is usually the cleanest for headless: bearer token, stateless, frontend-friendly.
Cache strategy
REST API + CDN:
Cache-Control: public, max-age=300response header (via Nginx or a plugin)- CDN edge cache
- Purge
/wp-json/wp/v2/posts/{id}when the post updates
GraphQL + Apollo/urql client cache:
- Server-side: WPGraphQL Smart Cache plugin (internal cache)
- Client-side: Apollo Cache or urql cache
- In Next.js: ISR regeneration (time-based, not request-by-request invalidation)
GraphQL doesn’t inherit REST’s HTTP-native cache. Invalidation gets more custom.
Maintenance comparison
Six to twelve months of project maintenance:
REST API: endpoints stay stable through WP major upgrades, breaking changes are rare. ACF REST exposure has to be maintained per field.
GraphQL: WPGraphQL plugin compatibility to watch. Custom type registration is an ongoing cost.
Both are manageable, but GraphQL has slightly more ceremony.
Final take
Decision matrix for a new headless WP project:
- Quick prototype: REST
- Production SaaS / mobile: GraphQL
- Public API exposure: REST
- ACF-heavy, complex schema: GraphQL
- Cache-critical: REST
- TypeScript-first: GraphQL
Both have pros and cons. It’s not “one right, one wrong”; it’s fit for the project.
My pattern: REST is enough for small-to-medium projects, and the GraphQL investment pays off on bigger, more complex ones.