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, not query 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/cache or unstable_cache in Next.js to cache query results and reduce load on the WordPress server.