Choosing between wpgraphql vs rest api headless architectures determines how your front end fetches data, how your edge CDN caches responses, and how much maintenance your WordPress backend requires. Both interfaces can serve decoupled front ends like Next.js, but they handle relational queries, caching layers, and field extensions in fundamentally different ways. This guide breaks down the real architectural trade-offs so you can pick the right data layer for your project.
Quick answer
- Choose WPGraphQL if your templates require deeply nested relational data (posts with authors, categories, featured media, and custom fields) fetched in a single network round-trip.
- Choose the core REST API if you need simple CRUD operations, want zero third-party plugin dependencies in WordPress, or rely heavily on out-of-the-box HTTP caching with edge CDNs like Cloudflare.
- ACF integration is built into core REST (fields surface under the
acfkey once toggled), whereas WPGraphQL requires thewpgraphql-acfextension plugin to expose fields to the schema. - Caching complexity is higher in WPGraphQL because queries default to HTTP POST, requiring persisted queries or plugins like WPGraphQL Smart Cache to enable standard HTTP GET edge caching.
WPGraphQL vs REST API headless: query architecture and payload size
The fundamental difference between the two APIs lies in how payloads are constructed. The WordPress REST API uses fixed endpoints. Requesting /wp/v2/posts returns a predetermined set of fields for every post, including rendered HTML, GUIDs, ping statuses, and comment counts that your Next.js application probably does not use.
The over-fetching problem in REST
When fetching a list of posts via REST, the response frequently exceeds 50KB to 100KB of JSON for ten posts. You can partially mitigate this using the _fields query parameter, but nested resources remain an issue:
# Fetch posts with restricted fields, but author and media details require extra calls
curl -X GET "https://cms.example.com/wp-json/wp/v2/posts?_fields=id,title,slug,featured_media,author"
To resolve the author's display name and the featured image URL, your front-end code must either issue parallel fetch requests to /wp/v2/users/:id and /wp/v2/media/:id, or use the _embed query parameter. Using _embed balloons the response size again and dumps raw media metadata into your payload.
Precise querying with WPGraphQL
WPGraphQL allows the front end to define the exact shape of the response. Nested relationships resolve in a single execution tree inside WordPress, returning only the requested keys.
query GetPostSummaries {
posts(first: 10) {
nodes {
id
title
slug
author {
node {
name
}
}
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
}
}
This query runs in a single round-trip and produces a clean JSON payload often under 5KB. For mobile clients or high-latency connections, eliminating waterfall requests provides an immediate advantage.
Exposing Advanced Custom Fields: schema mapping vs raw arrays
Most headless WordPress sites rely heavily on Advanced Custom Fields (ACF). The two APIs handle custom field serialization through completely different paradigms.
ACF in the REST API
Since modern ACF versions, core REST support is built-in. When editing an ACF Field Group in the WordPress admin, toggle Show in REST API to Yes. Once saved, all fields in that group automatically appear under the acf property of the corresponding post type endpoint.
To fetch post data including ACF fields in Next.js App Router:
// app/posts/[slug]/page.tsx
interface RestPost {
id: number;
title: { rendered: string };
slug: string;
acf: {
hero_subtitle?: string;
featured_stat?: number;
};
}
export async function getPost(slug: string): Promise<RestPost | null> {
const res = await fetch(
`${process.env.WORDPRESS_URL}/wp-json/wp/v2/posts?slug=${slug}&_fields=id,title,slug,acf`,
{ next: { revalidate: 60 } }
);
const posts = await res.json();
return posts[0] || null;
}
While simple, REST returns ACF fields as raw, untyped key-value pairs. Flexible content layouts and repeaters return nested arrays with minimal type metadata, making strict TypeScript typing manual and error-prone.
ACF in WPGraphQL
WPGraphQL does not read ACF fields automatically. You must install the WPGraphQL for ACF extension plugin. This plugin registers ACF field groups directly into your GraphQL schema under their designated GraphQL Field Name.
query GetPageWithAcf($slug: ID!) {
page(id: $slug, idType: URI) {
title
pageSettings {
heroSubtitle
featuredStat
flexibleContentBlocks {
__typename
... on PageSettingsFlexibleContentBlocksHeroLayout {
headline
ctaUrl
}
... on PageSettingsFlexibleContentBlocksAccordionLayout {
items {
question
answer
}
}
}
}
}
}
This setup allows union types and fragments on Flexible Content layouts. Your front-end schema tooling (such as GraphQL Code Generator) can generate exact TypeScript types for every flexible block, preventing runtime null reference errors.
Important: If you use WPGraphQL for ACF, always assign explicit, machine-safe identifiers in the "GraphQL Field Name" setting inside your ACF field groups. Leaving these blank or relying on auto-generated names breaks your schema whenever a field label changes.
Edge caching and network transport trade-offs
Performance in headless WordPress is determined primarily by your caching strategy. If every visitor request hits your WordPress application server, PHP execution time and database queries will degrade your time-to-first-byte (TTFB).
REST API caching at the CDN edge
The REST API communicates over standard HTTP GET requests. Every endpoint produces a unique, cacheable URL with predictable parameters:
GET /wp-json/wp/v2/posts?per_page=10&page=1
CDNs like Cloudflare, Fastly, or Varnish cache these responses effortlessly. You can attach standard cache control headers using WordPress hooks:
add_filter('rest_post_dispatch', function(WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request) {
if ($request->get_method() === 'GET' && !is_user_logged_in()) {
// Cache successful public GET responses for 10 minutes at the edge
$response->header('Cache-Control', 'public, s-maxage=600, max-age=60, stale-while-revalidate=30');
}
return $response;
}, 10, 3);
WPGraphQL caching challenges
By default, GraphQL clients send requests using HTTP POST to /graphql. Standard CDNs and reverse proxies do not cache HTTP POST requests because POST is designed for mutating data.
To make WPGraphQL performant under high traffic:
- Configure your client to use HTTP GET with URL-encoded query strings for read operations.
- Install the WPGraphQL Smart Cache plugin, which emits cache tags and Surrogate-Key headers for reverse proxies.
- Implement Persisted Queries, which replace massive query strings in URLs with a 32-character hash (e.g.,
/graphql?queryId=c1a2b3...).
Here is how to execute a GET-based query in the Next.js App Router for edge caching:
// lib/wpgraphql-client.ts
export async function fetchGraphQL<T>(query: string, variables: Record<string, any> = {}): Promise<T> {
const url = new URL(`${process.env.WORDPRESS_URL}/graphql`);
url.searchParams.set('query', query.replace(/\s+/g, ' ').trim());
if (Object.keys(variables).length > 0) {
url.searchParams.set('variables', JSON.stringify(variables));
}
const res = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
},
next: { revalidate: 300 },
});
if (!res.ok) {
throw new Error(`Failed to fetch GraphQL data: ${res.statusText}`);
}
const json = await res.json();
if (json.errors) {
throw new Error(json.errors[0]?.message || 'GraphQL execution error');
}
return json.data;
}
Authentication, mutations, and preview handling
Both APIs handle content authoring workflows and front-end previews, but their authentication mechanics differ.
Previewing draft posts
To display unpublished drafts in Next.js, your front end must authenticate against WordPress to bypass status checks. Drafts cannot be retrieved by unauthenticated public requests.
- REST API: Supports Application Passwords (built into WordPress core) or JWT authentication plugins. You query
/wp-json/wp/v2/posts/<id>?status=draftwith anAuthorization: Basic <credentials>orBearer <token>header. - WPGraphQL: Requires the WPGraphQL JWT Authentication plugin or cookie-based session handling. You request the post by database ID with
asPreview: truein your query variables.
Writing custom mutations vs custom endpoints
When handling form submissions, user registrations, or custom business logic, you must choose between extending the schema or creating REST endpoints.
Creating a custom REST endpoint in WordPress is straightforward and uses native core functions:
add_action('rest_api_init', function() {
register_rest_route('app/v1', '/contact', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'handle_custom_contact_submission',
'permission_callback' => '__return_true',
]);
});
function handle_custom_contact_submission(WP_REST_Request $request): WP_REST_Response {
$email = sanitize_email($request->get_param('email'));
if (!is_email($email)) {
return new WP_REST_Response(['message' => 'Invalid email address.'], 422);
}
// Execute business logic (e.g., save post, send alert)
return new WP_REST_Response(['success' => true], 200);
}
Registering a custom mutation in WPGraphQL requires significantly more boilerplate. You must define the mutation name, input fields, output fields, and a resolver function through register_graphql_mutation(). For sites with simple write operations, REST is often much faster to implement and debug.
Plugin dependencies and backend maintenance overhead
Every WordPress plugin introduces maintenance risk, potential security vectors, and update synchronization requirements.
The core REST advantage
The REST API is part of WordPress core. It receives constant security patches, adheres to core backward compatibility promises, and cannot be deactivated. If an agency hands off a site using the core REST API and built-in ACF toggles, the site can run on standard WordPress updates without breaking third-party schema dependencies.
The WPGraphQL plugin stack
WPGraphQL is not in WordPress core. A fully functioning headless build typically requires:
wp-graphql(Core schema engine)wpgraphql-acf(Exposing ACF to schema)wp-graphql-smart-cache(Edge caching and purge triggers)wp-graphql-jwt-authentication(For previews and user auth)
While the WPGraphQL community is active and these plugins are high quality, you are managing a specialized four-plugin stack. Major version upgrades to ACF or WordPress occasionally introduce schema deprecations or breaking changes that require plugin updates before your front end works smoothly again.
Decision framework: which should you choose?
Base your choice on your data structures and front-end requirements rather than ecosystem hype.
Choose WPGraphQL if: - Your front-end templates consume complex, relational data (e.g., authors with custom meta, deeply nested taxonomy terms, multiple ACF flexible content blocks, and linked post types). - You use TypeScript on the front end and want end-to-end schema validation using GraphQL Code Generator. - You want to minimize payload size over the wire to enhance mobile page speed. - Your developers are comfortable managing client-side caching libraries like Apollo Client or Urql, or structuring GET-based queries for Next.js.
Choose REST API if: - You want zero or minimal external plugin dependencies in WordPress. - Your site has a straightforward editorial structure (standard pages, simple post archives, isolated custom post types). - You rely heavily on existing CDN rules (Cloudflare, Fastly) that automatically cache standard GET requests without complex setup. - You need to build custom webhook listeners, third-party ingestion routes, or quick form submission handlers with minimal PHP boilerplate.
Checklist
- Audit the data relationships across your templates: count how many nested round-trips REST requires versus a single GraphQL query.
- Verify ACF setup: ensure Show in REST API is enabled for REST, or confirm WPGraphQL for ACF is active with unique GraphQL field names.
- Determine edge caching needs: verify whether your hosting and CDN stack can handle POST requests, or configure WPGraphQL to use HTTP GET with persisted queries.
- Review plugin dependencies: ensure your operations team is prepared to track updates across the WPGraphQL plugin ecosystem.
- Test draft and preview routes: establish your authentication layer (Application Passwords vs JWT) before writing template code.
Frequently asked questions
Is WPGraphQL faster than the WordPress REST API?
WPGraphQL is faster over the network because it eliminates over-fetching and resolves relational data in a single request. However, on uncached backend execution, WPGraphQL can introduce slight PHP execution overhead compared to highly targeted, indexed REST queries.
Can I cache WPGraphQL queries with Cloudflare?
Yes, but you must configure your client to send queries as HTTP GET requests rather than HTTP POST. Pair this with persisted queries or the WPGraphQL Smart Cache plugin to deliver consistent cache hits at the CDN edge.
Does ACF support headless WordPress without plugins?
Yes. ACF fields can be served via the native WordPress REST API without extra plugins by enabling the Show in REST API toggle in the field group settings. For WPGraphQL, the WPGraphQL for ACF plugin is required.
Can I use both REST API and WPGraphQL together?
Yes. Many architectures use WPGraphQL for complex page and post queries while using native WordPress REST endpoints for form submissions, checkout flows, or webhooks. The two systems do not conflict.
- Next.js documentationNext.js documentation
- WordPress REST API HandbookWordPress REST API Handbook
- WPGraphQL docsWPGraphQL docs
Drafted with AI assistance. Code targets current WordPress, WooCommerce and Next.js APIs; test changes on a staging site before production.