The WooCommerce REST API works. GraphQL works better — especially for frontends that need to fetch complex, nested product data in a single request. With WPGraphQL and the WooGraphQL extension, your headless frontend can request exactly the data it needs, nothing more, nothing less.

Setup

Install two plugins on your WordPress backend:

  • WPGraphQL — adds a /graphql endpoint to WordPress
  • WooGraphQL — extends WPGraphQL with WooCommerce types and queries

Your GraphQL endpoint: https://your-store.com/graphql

Why GraphQL Over REST for WooCommerce

Consider a product listing page. With REST, you need:

GET /wc/v3/products?per_page=12        → Products (basic info)

GET /wc/v3/products/categories → All categories (for filters)

GET /wc/v3/products/attributes → All attributes (for filters)

GET /wc/v3/products/{id}/variations → Variations (per product)

That’s 4+ API calls, each returning data you don’t need. With GraphQL:

query ProductListing($first: Int!, $after: String, $categoryIn: [Int]) {

products(first: $first, after: $after, where: { categoryIdIn: $categoryIn }) {

pageInfo {

hasNextPage

endCursor

}

nodes {

id

databaseId

name

slug

... on SimpleProduct {

price

regularPrice

salePrice

stockStatus

}

... on VariableProduct {

price

regularPrice

variations(first: 50) {

nodes {

databaseId

name

price

stockStatus

attributes {

nodes {

name

value

}

}

}

}

}

image {

sourceUrl(size: MEDIUM)

altText

}

productCategories {

nodes {

name

slug

}

}

}

}

}

One request. Exactly the fields you need. No over-fetching.

Essential Queries

Single Product

query GetProduct($slug: ID!) {

product(id: $slug, idType: SLUG) {

databaseId

name

slug

description

shortDescription

sku

... on SimpleProduct {

price

regularPrice

salePrice

stockQuantity

stockStatus

}

... on VariableProduct {

price

regularPrice

variations(first: 100) {

nodes {

databaseId

price

regularPrice

stockStatus

stockQuantity

attributes {

nodes { name value }

}

image {

sourceUrl(size: MEDIUM_LARGE)

altText

}

}

}

defaultAttributes {

nodes { name value }

}

}

galleryImages {

nodes {

sourceUrl(size: LARGE)

altText

}

}

productCategories {

nodes { name slug }

}

attributes {

nodes {

name

options

variation

}

}

related(first: 4) {

nodes {

name

slug

... on SimpleProduct { price }

image { sourceUrl(size: MEDIUM) }

}

}

}

}

Product Search with Filters

query SearchProducts(

$search: String,

$categoryIn: [Int],

$minPrice: Float,

$maxPrice: Float,

$orderBy: ProductsOrderByEnum,

$first: Int!

) {

products(

first: $first

where: {

search: $search

categoryIdIn: $categoryIn

minPrice: $minPrice

maxPrice: $maxPrice

orderby: { field: $orderBy, order: ASC }

}

) {

nodes {

databaseId

name

slug

... on SimpleProduct {

price

regularPrice

stockStatus

}

image {

sourceUrl(size: MEDIUM)

}

}

pageInfo {

total

hasNextPage

endCursor

}

}

}

Client Setup (Next.js + Apollo)

// lib/apollo-client.js

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

const client = new ApolloClient({

link: new HttpLink({

uri: process.env.NEXT_PUBLIC_GRAPHQL_URL,

credentials: 'include'

}),

cache: new InMemoryCache({

typePolicies: {

Query: {

fields: {

products: {

keyArgs: ['where'],

merge(existing, incoming) {

return incoming; // Replace on new query

}

}

}

}

}

})

});

export default client;

// pages/products/[slug].js

import { gql } from '@apollo/client';

import client from '../../lib/apollo-client';

const GET_PRODUCT = gql...; // Query from above

export async function getStaticProps({ params }) {

const { data } = await client.query({

query: GET_PRODUCT,

variables: { slug: params.slug }

});

return {

props: { product: data.product },

revalidate: 300 // ISR: regenerate every 5 minutes

};

}

export async function getStaticPaths() {

const { data } = await client.query({

query: gql

query { products(first: 100) { nodes { slug } } }

});

return {

paths: data.products.nodes.map(p => ({ params: { slug: p.slug } })),

fallback: 'blocking'

};

}

Mutations: Cart and Checkout

WooGraphQL provides mutations for cart operations:

mutation AddToCart($productId: Int!, $quantity: Int!) {

addToCart(input: { productId: $productId, quantity: $quantity }) {

cartItem {

key

product { node { name } }

quantity

total

}

cart {

total

subtotal

contentsCount

}

}

}

mutation Checkout($input: CheckoutInput!) {

checkout(input: $input) {

order {

databaseId

orderNumber

status

total

}

result

}

}

Performance Tips

  • Persisted queries: Pre-register your queries on the server to reduce payload size and prevent arbitrary queries
  • Query complexity limits: Set graphql_max_query_depth to prevent expensive nested queries
  • DataLoader pattern: WPGraphQL uses DataLoader internally, but custom resolvers should too
  • Fragment reuse: Define product card fragments and reuse across queries
  • Static generation: Use getStaticProps with ISR rather than client-side queries for product pages

fragment ProductCard on Product {

databaseId

name

slug

... on SimpleProduct { price salePrice stockStatus }

... on VariableProduct { price }

image { sourceUrl(size: MEDIUM) altText }

}

query LatestProducts {

products(first: 8, where: { orderby: { field: DATE } }) {

nodes { ...ProductCard }

}

}

GraphQL vs REST: When to Use Which

Scenario Recommended Why
Product listings (complex) GraphQL Fewer requests, exact fields
Cart operations REST (Store API) Better session handling
Simple CRUD REST Simpler implementation
Mobile apps GraphQL Bandwidth efficiency
Server-side rendering GraphQL Single request per page
Webhooks REST GraphQL doesn’t support push

Conclusion

WPGraphQL + WooGraphQL is the optimal data layer for headless WooCommerce when your frontend needs complex, nested product data. The learning curve is steeper than REST, but the payoff — fewer requests, exact data fetching, and type safety — makes it worth the investment for production headless stores. Use GraphQL for reads (products, categories, content) and REST for writes (cart, checkout, orders) to get the best of both worlds.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Search Window