WordPress REST API vs. GraphQL in 2026: The architectural showdown

WordPress REST API vs. GraphQL in 2026: The architectural showdown

Last verified: October 14, 2026
17 min read
Guide
Full-stack developer

The “Headless WordPress” revolution has matured. In 2026, the question is no longer if you should decouple your frontend, but how you should fetch your data. The choice sits between the native REST API in WordPress core and GraphQL, which on WordPress almost always means the WPGraphQL plugin.

The decision now rests on engineering requirements you can measure: how many requests a page needs, how much JSON the browser has to parse, how responses get cached at the edge, how the frontend gets its types, and who maintains the extra code. Both options are mature. This guide compares them on those points, with every API name, setting and default checked against the REST API Handbook and the WPGraphQL documentation.


#1. Differences between REST API and GraphQL in WordPress

REST (endpoint-driven)
  GET /wp-json/wp/v2/posts/101
  GET /wp-json/wp/v2/users/4
  GET /wp-json/wp/v2/categories?post=101
  = several HTTP requests, each with a fixed response shape

GraphQL (document-driven)
  POST /graphql
  query SinglePost {
    post(id: "101", idType: DATABASE_ID) {
      title
      date
      author { node { name } }
      categories { nodes { name slug } }
    }
  }
  = one HTTP request, the client names every field it wants

The REST API is built on fixed resources. A request to /wp-json/wp/v2/posts returns the full post representation: rendered content, excerpt, GUID, comment status, links and more. WPGraphQL inverts this. The client sends a query document, and the server returns only the fields that document names.

#Over-fetching and under-fetching in the REST API

The WPGraphQL team describes the REST problem as simultaneous over-fetching and under-fetching: the posts endpoint returns category IDs, tag IDs, content and excerpt that a list view does not need, yet still lacks the author name and avatar, which takes more requests (WPGraphQL vs WP REST API).

Core has two answers to this, and they are often overlooked. The _fields global parameter returns only a subset of fields, for example ?_fields=id,title,link, and since WordPress 5.3 it supports nested properties such as a single meta key. The _embed parameter includes linked resources (author, featured media, terms) in the same response, and since WordPress 5.4 you can limit it to specific link relations (Global parameters). With both, a REST list view can come close to a GraphQL payload. What REST still lacks is a typed schema the client can query against.


#2. WordPress REST API pros and cons

The WordPress REST API has been part of core for a decade, and it is more robust than ever.

  • The strength: It works out of the box. No extra plugins are required for basic functionality. It is highly cacheable at the HTTP level using standard CDNs, because every resource has its own URL.
  • The weakness: Over-fetching by default. If you only need a post title and slug and forget _fields, REST sends the whole post object, including content and metadata. This wastes bandwidth, especially on mobile.

The REST API uses JSON for requests and responses, and any language that can make HTTP requests and read JSON can use it (REST API Handbook). That is why almost every integration platform, CRM connector and webhook consumer speaks REST first.


#3. WPGraphQL pros and cons

GraphQL (usually implemented via WPGraphQL) allows the frontend developer to define the shape of the response.

  • The strength: One request, exactly the data you want. You can fetch a post, its categories, the author’s latest 3 posts and the site settings in a single network round-trip. The schema is strongly typed, and every field declares its type.
  • The weakness: It requires an additional plugin to maintain. Query complexity can sometimes lead to slow database lookups if it is not optimised with an object cache (Redis).

The plugin facts, read from the wordpress.org plugin directory on 14 October 2026: WPGraphQL 2.24.0, requires WordPress 6.0 and PHP 7.4, tested up to WordPress 7.0.7, more than 30,000 active installations. The plugin page states that WPGraphQL is becoming a Canonical Plugin on WordPress.org. It follows Semantic Versioning, so a major version bump signals breaking changes and is worth reading in the release notes before you update.


#4. How WPGraphQL solves the N+1 problem

A naive resolver for “the last 20 posts and each author’s name” runs one query for the posts and then one query per author. That is the N+1 problem. WPGraphQL avoids it with the DataLoader pattern: during resolution it collects the IDs it needs (all post_author values, for example) and loads them in one batch. In the WPGraphQL documentation’s own example, the SQL queries for five posts’ authors drop from five to one (WPGraphQL vs WP REST API).

SELECT * FROM wp_users WHERE ID IN (2, 5, 8, 14);

On top of that, WPGraphQL has a Model Layer: a set of PHP classes that wrap WordPress objects and check sensitive fields, such as a user’s email address, before they are returned (WPGraphQL concepts).

#How to extend the WPGraphQL schema in PHP

<?php
declare(strict_types=1);

add_action('graphql_register_types', function (): void {
    register_graphql_object_type('PerformanceMetrics', [
        'description' => __('Measured Core Web Vitals for a post', 'wppoland'),
        'fields' => [
            'lcpSeconds' => ['type' => 'Float'],
            'inpMilliseconds' => ['type' => 'Int'],
            'clsScore' => ['type' => 'Float'],
        ],
    ]);

    register_graphql_field('Post', 'cwvMetrics', [
        'type' => 'PerformanceMetrics',
        'resolve' => function ($post): ?array {
            $raw = get_post_meta($post->databaseId, '_wppoland_cwv_data', true);
            if (!is_array($raw)) {
                return null;
            }
            return [
                'lcpSeconds' => isset($raw['lcp']) ? (float) $raw['lcp'] : null,
                'inpMilliseconds' => isset($raw['inp']) ? (int) $raw['inp'] : null,
                'clsScore' => isset($raw['cls']) ? (float) $raw['cls'] : null,
            ];
        },
    ]);
});

#5. How to create a custom REST endpoint in WordPress

The core pattern is register_rest_route() on the rest_api_init hook, or a class that extends WP_REST_Controller. The handbook rules that matter in production (Adding custom endpoints):

  • Since WordPress 5.5, a route registered without permission_callback triggers a _doing_it_wrong notice. For a public endpoint, pass __return_true explicitly.
  • Namespaces should follow the vendor/v1 pattern, and the version goes up when you break compatibility.
  • Errors are a WP_Error with the HTTP code set in the status key of its data, for example 400 for bad input.
  • When you wrap an existing callback, run its return value through rest_ensure_response().
<?php
declare(strict_types=1);

class Wppoland_Metrics_Controller extends WP_REST_Controller
{
    protected $namespace = 'wppoland/v1';
    protected $rest_base = 'metrics';

    public function register_routes(): void
    {
        register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_item'],
                'permission_callback' => '__return_true',
                'args' => [
                    'id' => ['type' => 'integer', 'required' => true],
                ],
            ],
            'schema' => [$this, 'get_public_item_schema'],
        ]);
    }

    public function get_item($request)
    {
        $post = get_post((int) $request['id']);
        if (!$post || $post->post_status !== 'publish') {
            return new WP_Error('rest_post_invalid_id', __('Not found.', 'wppoland'), ['status' => 404]);
        }
        return rest_ensure_response([
            'id' => $post->ID,
            'title' => get_the_title($post),
        ]);
    }

    public function get_item_schema(): array
    {
        return [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'wppoland_metrics',
            'type' => 'object',
            'properties' => [
                'id' => ['type' => 'integer'],
                'title' => ['type' => 'string'],
            ],
        ];
    }
}

add_action('rest_api_init', function (): void {
    (new Wppoland_Metrics_Controller())->register_routes();
});

The properties are declared without a type on purpose: WP_REST_Controller declares $namespace and $rest_base untyped, and PHP refuses a typed redeclaration in a child class. For the full controller walkthrough see our guide to custom REST API endpoints.


#6. Is GraphQL faster than REST in WordPress

In 2026, performance isn’t just about speed; it’s about efficiency.

  • Network latency: GraphQL wins on complex pages. By bundling multiple data points into one request, it removes the sequential round-trips that a REST page with post, author, terms and related posts needs.
  • Caching strategy: REST wins on ease of use. Since every endpoint is a unique URL, you can cache it globally on Cloudflare with little effort. GraphQL needs GET requests or persisted queries to achieve similar edge-caching results.
  • Server CPU: REST is usually lighter for one simple resource. GraphQL has to parse and validate the query document before it resolves anything, which is why an object cache matters more on a GraphQL site.

The WPGraphQL team published one measurement: fetching 100 posts took 335 kb and 7.91 s from the REST endpoint against 6.4 kb and 67 ms from WPGraphQL (WPGraphQL vs WP REST API). Treat that as the vendor’s own test with default REST output. A REST request with _fields closes much of the payload gap, so measure your own pages before you choose on this argument alone.


#7. How to cache GraphQL at the CDN with persisted queries

GraphQL queries are usually sent as HTTP POST with the query in the body, and a CDN does not cache POST by URL. WPGraphQL’s own guidance is that POST is the typical method and GET has limited support (Interacting with WPGraphQL). Edge caching is handled by the companion plugin WPGraphQL Smart Cache (version 2.3.3, more than 6,000 active installations, now part of the WPGraphQL monorepo, per its plugin page).

How it works, according to the Smart Cache documentation:

  • Network cache: to benefit from it you send HTTP GET requests for public data. Responses carry an X-GraphQL-Keys header with tags for everything the query touched. When content changes, Smart Cache sends purge events, and the host evicts the cached documents tagged with those keys. The guide lists WP Engine and Pantheon as supporting hosts. The default max-age is 600 seconds, adjustable in the plugin’s Cache settings (Network cache).
  • Persisted queries: a query document is stored on the server in a non-public graphql_document post type and referenced by a query ID. The document slug is the SHA-256 hash of the query, and a client can register a new queryId only if it matches that hash. Clients then send short GET requests such as ?queryId=getPostById&variables={...}. The plugin works with the Apollo Persisted Query Link (Persisted queries).
  • Allow and deny lists: under the saved queries settings you choose between a fully public endpoint, allowing only specific saved queries, or denying specific ones. Users with manage_options are exempt.

With GET requests, persisted queries and tag-based purging in place, a GraphQL response is cached at the edge much like a REST URL. Without them, GraphQL caching falls back to the WordPress object cache.


#8. WordPress REST batch API vs GraphQL batching

WordPress 5.6 added a batch framework at /wp-json/batch/v1. It accepts one POST with several sub-requests, 25 by default (filter rest_get_max_batch_size). GET requests are not supported, so it is meant for writes, and each route must opt in with allow_batch when it is registered. A require-all-validate mode rejects the whole batch if any request fails validation (REST API batch framework in WordPress 5.6).

#Limitations of batching in the REST API

  1. No data composition: sub-requests run as separate operations. You cannot feed the result of request A (an author ID) into request B in the same batch.
  2. Write-oriented: without GET, the batch endpoint does not help a read-heavy frontend at all.
  3. Not cacheable at the edge: it is a POST, so a CDN passes every batch to the origin.

GraphQL resolves related data inside one query by design, and WPGraphQL also accepts batched queries. Its security documentation notes that you can limit or disable batch queries on the GraphQL settings page.


#9. Pagination limits in REST and GraphQL

The REST API caps per_page at 100 records and returns two headers with each collection: X-WP-Total (total records) and X-WP-TotalPages (total pages) (Pagination). A client that needs 450 posts makes five requests.

WPGraphQL uses cursor-based connections (first, after, pageInfo { hasNextPage endCursor }). It caps the items per connection page at 100 for most connections, and the graphql_connection_max_query_amount filter changes that cap (WPGraphQL security). Raise it only together with the depth limit and caching, because larger pages make expensive queries cheaper to send.


#10. Authentication in the WordPress REST API and WPGraphQL

The REST API has two built-in methods (Authentication):

  • Cookie authentication for code running in the logged-in dashboard. A manual request must send a nonce for the wp_rest action, usually in the X-WP-Nonce header. Without the nonce the request is treated as unauthenticated, even if the user is logged in.
  • Application Passwords, shipped in core since WordPress 5.6. They use Basic Auth and work only over HTTPS.

WPGraphQL follows WordPress permissions: anything WordPress exposes publicly is public in GraphQL, and data that needs authentication in WordPress needs an authenticated GraphQL request. Users without published posts are excluded from public user queries, and fields such as a user’s email require specific permission (WPGraphQL security). Token-based login flows (JWT and similar) come from extensions, not from the core plugin.


#11. How to secure the REST API and GraphQL in WordPress

MeasureWPGraphQLREST API
Schema discoveryPublic introspection is disabled by default; enable it in GraphQL settings only if you need itThe /wp-json/ index lists routes; require authentication with rest_authentication_errors if the site has no public consumers
Query depthNew installs limit depth to 15; filter graphql_query_depth_maxNot applicable, response shapes are fixed per endpoint
Query costCost-based complexity limits are not in the plugin yet (tracked in issue #3922)Not applicable
Page size100 items per connection page by defaultper_page capped at 100
BatchingBatch queries can be limited or disabledRoutes must opt in with allow_batch
Write authenticationWordPress permissions; tokens via extensionsCookie plus nonce, or Application Passwords over HTTPS

Sources: WPGraphQL security, REST API Handbook FAQ. The handbook’s way to block anonymous REST access is an is_user_logged_in() check on the rest_authentication_errors filter. Rate limiting belongs at the edge (WAF or CDN) for both APIs, because neither plugin nor core ships one.


#12. Generate TypeScript types from WPGraphQL

Because the GraphQL schema is typed and introspectable, tools such as GraphQL Code Generator can read the schema and the frontend’s queries and generate TypeScript types for every query result. A field that becomes nullable or disappears from the schema then fails the type check in the editor and in CI, before it reaches production.

import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { print } from 'graphql';

export async function executeGraphQL<TData, TVariables>(
  document: TypedDocumentNode<TData, TVariables>,
  variables?: TVariables
): Promise<TData> {
  const response = await fetch(import.meta.env.WP_GRAPHQL_ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: print(document), variables }),
  });
  if (!response.ok) throw new Error(`GraphQL HTTP ${response.status}`);
  const { data, errors } = await response.json();
  if (errors?.length) throw new Error(errors[0].message);
  return data;
}

The REST API publishes a JSON Schema for each route, so types can be generated there too, but you assemble them per endpoint and the server does not validate your client’s field selection.


#13. Error handling in the REST API and GraphQL

A REST error is a WP_Error serialised as JSON with code, message and data.status, and the HTTP status of the response matches data.status. If the request fails, the whole response is an error. That is predictable for monitoring, but a page built from one REST call cannot render half of its data.

GraphQL can return partial data. The response carries both data and errors, and each error has a path that points to the field that failed:

{
  "data": { "post": { "title": "Headless WordPress architecture", "relatedPosts": null } },
  "errors": [
    { "message": "Internal server error", "path": ["post", "relatedPosts"] }
  ]
}

The frontend can render the article and show a fallback only for the block that failed. The flip side: a GraphQL request that partly failed still returns HTTP 200, so monitoring has to read the errors array, not only status codes.


#14. GraphQL mutations and optimistic UI in WordPress

Writes in GraphQL are mutations. A mutation changes data and returns the updated object in the same response, so the client can refresh its local state without a second request. Core WPGraphQL ships mutations for posts, terms, users and comments, for example createComment:

mutation AddComment($input: CreateCommentInput!) {
  createComment(input: $input) {
    success
    comment { id content date }
  }
}

This makes optimistic UI simple: the interface shows the new comment immediately and rolls back only if the mutation returns an error. WooCommerce cart mutations are not part of WPGraphQL core; they come from a separate extension. With REST the same flow is a POST followed by reading the returned resource, which works just as well for single writes.


#15. GraphQL or REST for AI agents and the Abilities API

AI agents and MCP clients are now regular API consumers. GraphQL introspection lets an agent read the schema and see which types and arguments exist, but WPGraphQL keeps public introspection off by default, so an agent needs authenticated access or an exported schema.

WordPress core went another way. The Abilities API, in core since WordPress 6.9 (2 December 2025), registers operations with an input and output schema, a permission callback and an execute callback, and exposes them through the wp-abilities/v1 REST endpoints and the MCP Adapter (Abilities API in WordPress 6.9). For agents that act on a site, this REST-based layer is the native path. More detail in our guide to the WordPress Abilities API.


#16. Testing WordPress APIs with GraphiQL and Postman

  • REST: Familiar to almost every developer. You can test endpoints in a browser or tools like Postman instantly. Add ?_envelope when a client cannot read headers: the response then carries headers and status code in the body.
  • GraphQL: WPGraphQL ships GraphiQL, an in-browser IDE in the WordPress admin that lets you browse the entire schema, see available fields and test queries and mutations with auto-complete. For frontend teams this is a real productivity gain.

For regression safety, compare the frontend’s queries against the schema in CI on every merge, so a removed or retyped field fails the build. On the REST side, validate responses against each route’s JSON Schema.


#17. How to migrate from the REST API to WPGraphQL without downtime

  1. Coexistence: install WPGraphQL on staging with wp plugin install wp-graphql --activate and map every custom post type and taxonomy into the schema. REST keeps serving production.
  2. Read-only pages first: move archive and listing pages to GraphQL queries, with generated types, and compare payload and response time against the REST version.
  3. Detail pages and caching: move single views, then add Smart Cache with GET requests or persisted queries if your host supports network caching.
  4. Clean up REST exposure: keep REST routes that webhooks, automations and integrations use, and require authentication where no public consumer remains.

#18. REST vs. GraphQL comparison table

FeatureREST APIGraphQL (WPGraphQL)
SetupNative (built-in)Needs plugin
Data fetchingFixed endpointsFlexible, client-defined
Over-fetchingDefault, reduced with _fieldsAvoided by the query
Related dataExtra requests or _embedOne query
CachingEasy (URL-based)GET requests or persisted queries
TypingJSON Schema per route, not enforced on the clientStrongly typed schema
Page size cap100 per page100 per connection page
Ideal forSimple apps, webhooks, integrationsComplex JS apps, headless

#When to use GraphQL instead of REST in WordPress

If you are building a modern, decoupled site using React, Astro, or Next.js, GraphQL is usually the better fit. The ability to request exactly what is needed, with generated types, makes for a leaner and more maintainable frontend.

However, don’t ignore REST. For simple integrations, mobile apps that only need a feed of posts, internal automation, webhooks and AI agents using the Abilities API, the native REST API remains the fastest way to get the job done, with no plugin to maintain. Many production sites run both: GraphQL for the public frontend, REST for integrations.

Learn more about website migration to Astro and Next.js at WPPoland.

Are you still fetching the whole post object just for a title? Add _fields today, and plan the move to GraphQL when the frontend outgrows it.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you are planning a Headless WordPress setup, frontend decoupling, or migration to Astro, I can design and build the architecture, API, and frontend.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Article FAQ

Frequently asked questions

Practical answers to apply the topic in real execution.

SEO-readyGEO-readyAEO-ready4 Q&A
Is GraphQL faster than REST in WordPress?#
GraphQL is often faster for the *client* because it reduces the number of round-trips to the server. However, it can be more CPU-intensive for the *server* due to query parsing.
Do I need a plugin for GraphQL in WordPress?#
Yes, in 2026 WPGraphQL is still the industry standard plugin that adds a GraphQL schema to your WordPress site.
Can I use both REST and GraphQL on the same site?#
Absolutely. Many developers use GraphQL for the frontend display and REST for specific administrative tasks or external webhooks.
Which should I choose for a small blog?#
For a standard blog, the native REST API is usually sufficient and simpler to implement. GraphQL shines in complex, data-heavy applications.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles