GraphQL and REST: two approaches to data access
The WordPress REST API exposes fixed endpoints: each URL returns a predefined data structure. GraphQL works differently: a single endpoint (/graphql) receives queries that describe exactly the data desired. The server returns precisely that data, nothing more.
The main issue with the REST API in a headless context is unnecessary data transfer (overfetching). To display a list of articles with title, slug, and image, the REST API returns the full content of each article: complete body, metadata, author data, etc. With GraphQL, the query specifies exactly title, slug, and featuredImage, and the response only contains those fields.
Installing WPGraphQL
WPGraphQL is a free, open-source plugin that adds a GraphQL server to WordPress. It is available in the official WordPress plugin directory.
Install the plugin
In the WordPress back-office, go to Plugins > Add New. Search for "WPGraphQL" and click "Install". Activate the plugin once installation is complete.
Verify the endpoint
The GraphQL endpoint is immediately available at https://your-site.com/graphql. You can test it in a browser: a JSON error page confirms the GraphQL server is active.
Access GraphiQL IDE
A new "GraphQL" tab appears in the WordPress menu. It provides access to GraphiQL, an interactive environment that lets you compose, test, and document GraphQL queries directly in the browser.
Configure settings
In GraphQL > Settings, you can customize the endpoint path, enable debug mode, and configure which content types are exposed.
Writing GraphQL queries
Basic query: fetching posts
query LatestPosts {
posts(first: 10) {
nodes {
id
title
slug
date
excerpt
}
}
}
This query returns exactly 5 fields for each of the 10 most recent posts. In REST, the same operation would return more than 30 fields per post.
Query with nested relations
GraphQL resolves relations in a single query. To fetch posts with their author and featured image:
query CompletePosts {
posts(first: 6) {
nodes {
title
slug
date
author {
node {
name
avatar {
url
}
}
}
featuredImage {
node {
sourceUrl
altText
mediaDetails {
width
height
}
}
}
categories {
nodes {
name
slug
}
}
}
}
}
In REST, this same combination of data would require at least 3 separate queries (posts, authors, media) or use of the _embed parameter, which returns a far larger volume of data than needed.
Querying pages
query PageBySlug {
pageBy(uri: "about") {
title
content
date
featuredImage {
node {
sourceUrl
altText
}
}
}
}
Fetching media
query ImageGallery {
mediaItems(first: 20) {
nodes {
sourceUrl
altText
mediaDetails {
width
height
sizes {
name
sourceUrl
width
height
}
}
}
}
}
Variables and fragments
Variables
Variables make queries reusable by separating the query structure from dynamic values.
query PostBySlug($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
date
author {
node {
name
}
}
}
}
Associated JSON variables:
{
"slug": "my-headless-article"
}
In JavaScript (Next.js side):
async function fetchPost(slug) {
const query = `
query PostBySlug($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
date
author {
node { name }
}
}
}
`;
const response = await fetch('https://your-site.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { slug } })
});
const { data } = await response.json();
return data.post;
}
Fragments
Fragments avoid repeating common fields across multiple queries.
fragment PostInfo on Post {
title
slug
date
excerpt
featuredImage {
node {
sourceUrl
altText
}
}
}
query HomeAndBlog {
recentPosts: posts(first: 3) {
nodes {
...PostInfo
}
}
popularPosts: posts(first: 3, where: { orderby: { field: COMMENT_COUNT, order: DESC } }) {
nodes {
...PostInfo
}
}
}
Cursor-based pagination
WPGraphQL uses cursor-based pagination, conforming to the Relay specification. This system is more reliable than offset-based pagination because it isn't affected by content insertion or deletion between two queries.
query PaginatedPosts($after: String) {
posts(first: 10, after: $after) {
pageInfo {
hasNextPage
endCursor
}
nodes {
title
slug
date
}
}
}
To fetch the next page, pass the endCursor value into the $after variable of the next query. Repeat until hasNextPage is true.
Pagination and static generation
With SSG (Static Site Generation) in Next.js, cursor-based pagination lets you pre-generate every listing page at build time. Loop over the queries following the cursors until hasNextPage is false, then generate a static page for each result batch.
Mutations: writing data
Mutations let you create, modify, or delete content. They require authentication.
mutation CreatePost($title: String!, $content: String!) {
createPost(input: {
title: $title
content: $content
status: DRAFT
}) {
post {
id
title
slug
status
}
}
}
Authenticating mutations
Mutations require an authenticated WordPress user with the appropriate permissions. Use a plugin such as WPGraphQL JWT Authentication or pass an Application Password via the Authorization header. Mutations should never be executed from the visitor's browser.
Integration with Advanced Custom Fields
The WPGraphQL for ACF plugin automatically exposes ACF fields in the GraphQL schema. Once installed, custom fields are accessible in queries.
query PortfolioProject($slug: ID!) {
projet(id: $slug, idType: SLUG) {
title
projetFields {
client
annee
url
technologies
description
}
featuredImage {
node {
sourceUrl
}
}
}
}
The projetFields field corresponds to the ACF field group assigned to the "Project" content type. Each ACF field is automatically typed in the GraphQL schema (String, Int, Float, Boolean, MediaItem, etc.).
Performance: REST vs GraphQL by the numbers
1
query
GraphQL resolves posts + authors + images in a single query
-60%
data transferred
Average reduction in JSON volume compared to REST with _embed
3-5
REST queries
Number of queries needed to fetch the same nested data
Best practices
- Use explicit names for your queries (
query LatestPosts, notquery Q1): it makes debugging and monitoring easier. - Limit query depth to avoid overly expensive queries on the server. WPGraphQL includes default protection against excessively deep queries.
- Use fragments for recurring data structures in your application.
- Enable HTTP caching (
Cache-Control) or an object cache plugin (Redis) on the WordPress side to speed up GraphQL responses. - Combine WPGraphQL with
next/cacheorunstable_cachein Next.js to cache query results and reduce load on the WordPress server.