The choice between the WordPress REST API and GraphQL (via WPGraphQL) shapes your entire data-fetching architecture — how you write queries, how you handle caching, how your frontend team explores available data, and how much payload travels over the network on every request. Getting this decision right early saves significant refactoring later.
Both are production-ready and used on large-scale WordPress sites. The question isn’t which is better in the abstract — it’s which fits your project’s specific constraints. This guide covers both in enough depth to make that call confidently.
The WordPress REST API: Zero Setup, Predictable Caching
The REST API ships with WordPress core since version 4.7. Every WordPress installation has it running — no plugin required, no configuration step. Endpoints follow standard REST conventions: GET /wp-json/wp/v2/posts for a list, GET /wp-json/wp/v2/posts/123 for a single post, POST for creating, PUT/PATCH for updating.
The most important REST API optimisation is the _fields query parameter — it limits the response to only the fields you specify and cuts payload size by 60–70% compared to a full post object. A full /wp/v2/posts response returns ~30 fields including raw content, rendered content, author ID, category IDs, tag IDs, sticky flag, ping status, comment status, and more. Most headless frontends need none of that.
// REST API — fetch posts with specific fields only
// The _fields parameter cuts payload size by 60-70%
const res = await fetch(
'https://api.yourdomain.com/wp-json/wp/v2/posts' +
'?per_page=10&_fields=id,title,slug,excerpt,date,featured_media' +
'&_embed=wp:featuredmedia',
{ next: { revalidate: 3600 } } // Next.js ISR
);
const posts = await res.json();
// posts[0].title.rendered, posts[0]._embedded['wp:featuredmedia'][0].source_url
The _embed=wp:featuredmedia parameter embeds the featured image data in the same response rather than requiring a second request to the media endpoint — the one case where REST makes multiple-resource fetching convenient without an extra round trip.
Caching is where REST has a structural advantage: every endpoint is a unique URL, which means standard HTTP caching at the CDN layer works without any special configuration. Vercel, Cloudflare, and nginx all cache GET requests to unique URLs automatically. For a Next.js site using ISR, the next: { revalidate } option on fetch directly maps onto the REST URL.
The REST API’s main limitation is over-fetching on complex data shapes. If you need a post list with its author’s name, the featured image URL, and the first three tags — all rendered — you’re looking at either a bloated single request or multiple sequential requests. The _fields parameter reduces the payload but doesn’t solve the relational data problem. For sites where this matters, the custom REST endpoints guide covers building dedicated endpoints that return exactly the shape your frontend needs.
WPGraphQL: Exact Data, Single Request, Schema Explorer
GraphQL is a query language where the client defines the exact shape of the response. WPGraphQL is the standard WordPress implementation — a free plugin that exposes your WordPress data (posts, pages, menus, custom post types, taxonomies) as a typed GraphQL schema accessible at /graphql.
The same post list query in GraphQL:
// WPGraphQL — same query in GraphQL (single request, exact fields)
const POSTS_QUERY = `
query GetPosts {
posts(first: 10) {
nodes {
id
title
slug
excerpt
date
featuredImage {
node {
sourceUrl
altText
}
}
}
}
}
`;
const res = await fetch('https://api.yourdomain.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: POSTS_QUERY }),
next: { revalidate: 3600 },
});
const { data } = await res.json();
// data.posts.nodes[0].title, data.posts.nodes[0].featuredImage.node.sourceUrl
The response contains exactly the fields in the query — nothing more. No filtering parameters, no _fields trick needed. If the frontend needs title and slug for a card, that’s all that’s returned. If it needs the full content for a single post page, it adds that field to the query. The server adjusts accordingly.
The single-endpoint architecture also solves the relational data problem: you can fetch a post, its author’s name, its featured image URL, and its first three tags in one query without embedding or extra requests. For complex data shapes — menus, nested custom post types, ACF relational fields — this compounds into a significant reduction in network requests compared to the REST API.
WPGraphQL’s built-in IDE (accessible at /graphql when WP_DEBUG is enabled) lets frontend developers explore the full schema, autocomplete fields, and test queries without needing backend documentation. This speeds up frontend development significantly on teams where the frontend and backend developers work separately.
Caching is where GraphQL requires more work: most GraphQL requests use POST to a single /graphql endpoint. POST requests aren’t cached by CDNs or HTTP caches by default. Workarounds include persisted queries (pre-registered query IDs sent as GET requests, compatible with CDN caching), Apollo Client’s in-memory cache, or Vercel’s edge config for custom cache rules on the GraphQL endpoint. None of these are automatic the way REST URL caching is.
ACF Custom Fields: Both APIs Require Extra Work
The REST API doesn’t expose ACF custom fields by default — they exist in the WordPress database but aren’t included in REST responses. You have two options: use the ACF REST API setting in ACF Pro (which adds an acf object to each post response), or register custom REST fields manually:
// Exposing ACF fields via REST API — add to functions.php
add_action('rest_api_init', function () {
register_rest_field('post', 'acf_fields', [
'get_callback' => function ($post) {
return [
'custom_field' => get_field('custom_field', $post['id']),
'another_field' => get_field('another_field', $post['id']),
];
},
'schema' => ['type' => 'object'],
]);
});
// Fetch with: ?_fields=id,title,slug,acf_fields
WPGraphQL also doesn’t expose ACF fields by default — you need the WPGraphQL for ACF extension (separate plugin), then enable “Show in GraphQL” per field group in the ACF field group settings. Once enabled, fields appear in the WPGraphQL schema automatically.
Both approaches work, but the WPGraphQL + ACF extension path is cleaner for complex field groups — you get type safety, schema documentation, and nested object support (relational fields, repeater sub-fields) without writing custom PHP registration code for each field.
Direct Comparison
| Factor | REST API | WPGraphQL |
|---|---|---|
| Setup | Zero — built into core | Plugin install + optional ACF extension |
| Payload control | Via _fields parameter |
Exact — client defines response shape |
| Relational data | Multiple requests or _embed |
Single query across related types |
| CDN caching | Automatic (unique URLs) | Requires persisted queries or workarounds |
| Schema exploration | No built-in UI | GraphQL IDE at /graphql |
| ACF support | Via custom field registration or ACF Pro setting | Via WPGraphQL for ACF plugin |
| Learning curve | Low (standard HTTP) | Medium (GraphQL query syntax) |
| Best fit | Simple sites, small teams, fast setup | Complex sites, large frontends, data-heavy UIs |
When REST API Wins
- You’re building a simple blog or brochure site — 5–20 page types, minimal relational data. REST with
_fieldscovers it cleanly. - Your team knows REST but not GraphQL — the learning curve for GraphQL query syntax, type systems, and schema exploration is real. If you’re under a deadline, the zero-setup path wins.
- CDN caching is a hard requirement — if you can’t configure persisted queries or Vercel edge cache rules, REST’s URL-based caching is the path of least resistance.
- You’re using Next.js App Router with ISR — the
next: { revalidate }cache option onfetchworks directly with REST URLs. The Next.js headless performance guide covers this pattern in detail.
When WPGraphQL Wins
- You have complex relational data — posts with authors, featured images, custom taxonomies, ACF relational fields, and menu items all in one UI. A single GraphQL query replaces 4–6 REST requests.
- Your frontend team is large or changes frequently — the self-documenting schema and IDE mean frontend developers can explore available data independently without asking the WordPress developer for endpoint documentation.
- You’re using Gatsby — Gatsby’s data layer is GraphQL-native; WPGraphQL integrates directly via
gatsby-source-wordpressand is the recommended approach for Gatsby + WordPress. - You’re building a complex menu-driven site — WordPress menus are awkward in the REST API (no dedicated endpoint for rendered nav menus with children). WPGraphQL exposes menus and their nested items cleanly.
The Hybrid Approach
Nothing forces a binary choice. Some production headless WordPress sites use REST for straightforward content fetching (post lists, single posts, categories) — where _fields makes payloads efficient enough — and WPGraphQL for complex UI components that need relational data in one request (header menus, sidebar widgets with recent posts + categories + author info).
This hybrid works well in Next.js: REST calls go through the ISR fetch with URL-based caching; GraphQL calls go through a server component with Apollo’s server-side cache. The REST API handles the high-traffic content pages; GraphQL handles the layout data fetched once at build time.
Whichever gateway you choose, the rest of the headless stack connects to it the same way: JWT authentication secures the authenticated endpoints on both, webhooks trigger cache revalidation when WordPress content changes, and the headless SEO guide covers how to get metadata (canonical URLs, Open Graph, JSON-LD) into your Next.js pages regardless of which API delivers the content data.


