14+ years building on WordPress / Replies in under 5 hours
Headless WordPress 7 min read · Updated July 2026

WordPress REST API vs GraphQL (WPGraphQL): Which Should You Use?

AK
Ajay Khandal
WordPress Developer
Choosing Your Gateway: REST API vs. GraphQL (WPGraphQL)
TL;DR

Both the WordPress REST API and WPGraphQL (via the WPGraphQL plugin) are production-ready for headless WordPress sites — the difference is in payload control, caching, and relational data. REST is zero-setup, uses unique URLs that CDNs cache automatically, and works cleanly with the _fields parameter to cut payload size by 60-70%. WPGraphQL lets the frontend define the exact response shape in a single query, solves the relational data problem without multiple requests, and provides a self-documenting schema IDE — but requires a plugin, optional ACF extension for custom fields, and custom caching configuration (persisted queries or Vercel edge config) since POST requests to a single /graphql endpoint aren't cached by CDNs by default. Choose REST for simple sites, small teams, or where CDN caching is a hard requirement. Choose WPGraphQL for complex data shapes, large frontend teams, or Gatsby-based builds. The two aren't mutually exclusive — some production sites use REST for content pages and GraphQL for complex layout data.

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 _fields covers 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 on fetch works 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-wordpress and 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.

Frequently asked questions

It depends on your site's complexity and team. Use the REST API if you're building a simple site with straightforward content types, your team is more familiar with standard HTTP REST patterns than GraphQL, or you need reliable CDN caching without extra configuration — REST's unique URLs cache automatically at the CDN layer. Use WPGraphQL if you have complex relational data (posts with nested custom fields, menus with children, multiple content types in one view), a large frontend team that benefits from a self-documenting schema explorer, or you're building with Gatsby, which is GraphQL-native. The two aren't mutually exclusive — some production sites use REST for content pages and GraphQL for complex UI components.

WPGraphQL is a free WordPress plugin that exposes your WordPress content as a GraphQL API at /graphql. Unlike the REST API (which returns a fixed set of fields per endpoint), GraphQL lets the client define exactly which fields to request — if you only need title and slug for a post card, that's all the server returns. WPGraphQL also lets you fetch data across related types in a single query: a post's author name, featured image URL, and category names all in one request, rather than the multiple requests or _embed workarounds the REST API requires for the same data. The trade-off is that WPGraphQL requires a plugin, custom caching setup, and GraphQL query syntax knowledge.

Not by default — you need the WPGraphQL for ACF extension (a separate free plugin). Once installed, you enable 'Show in GraphQL' per field group in ACF's field group settings, and those fields appear in the WPGraphQL schema automatically with correct types (text, image, repeater sub-fields, relational fields). The REST API also doesn't expose ACF fields by default — you either use the ACF Pro REST API setting (which adds an acf object to responses) or register custom REST fields manually in functions.php using register_rest_field(). For complex field groups with nested repeaters or relational fields, the WPGraphQL + ACF extension approach is cleaner than custom REST registration.

The WordPress REST API uses unique URLs per resource — /wp-json/wp/v2/posts?slug=my-post is a distinct URL that CDNs, browsers, and Next.js fetch() all cache automatically using standard HTTP GET caching. WPGraphQL typically uses POST requests to a single /graphql endpoint, and POST requests aren't cached by HTTP caches or CDNs by default. The workarounds: persisted queries (pre-register your queries on the server and send a hash ID via GET, making them CDN-cacheable), Apollo Client's in-memory cache (client-side, not edge-cached), or custom Vercel edge cache rules for the /graphql endpoint. If CDN caching without extra configuration is a priority, REST is the simpler choice.

Yes. Nothing prevents you from using both in the same Next.js project. A common pattern: use the REST API with _fields for standard content pages (post lists, individual posts, category archives) where payload control via _fields is sufficient and ISR fetch caching works cleanly. Use WPGraphQL for complex components that need relational data in a single request — navigation menus with nested children, sidebars that combine recent posts, categories, and author info, or any UI where you'd otherwise need 3+ REST requests. This hybrid approach gets you the caching simplicity of REST for high-traffic content routes and the data-fetching power of GraphQL for complex layout components.

It depends on what you're measuring. For raw server execution time per query, they're comparable — both hit the WordPress database and PHP stack. GraphQL is often faster from the user's perspective because it eliminates multiple round trips: fetching a post, its author name, its featured image, and its categories requires 1 GraphQL query vs 3-4 REST requests. On the other hand, a simple REST request with _fields set correctly can be faster than a complex GraphQL query that resolves multiple related types, because the resolver chain adds overhead. The _fields parameter on the REST API closes much of the payload size gap that GraphQL's precision addressing provides — on simpler sites, the performance difference is minimal.

AK

Written by Ajay Khandal

WordPress Developer — building, fixing and speeding up WordPress sites.

Work with me →