🇹🇷 Türkçe: Bu yazının Türkçesini oku →

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.

Let me be clear about the recommendation up front, because the rest of this post is detail underneath it: if you are building a headless WooCommerce storefront and your product pages are read-heavy and deeply nested, I would put GraphQL on the reads and leave REST on the writes. That is not a fashion call. It is the split that has survived contact with production for me, and I will show you exactly where the line sits.

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

Ask for exactly what you need — an old discipline in a new syntax

The reason GraphQL clicks for me has nothing to do with it being new. It clicks because it enforces a discipline I learned the hard way, long before REST existed. In the mid-90s I built multi-user games — several players sharing one live session over links measured in kilobits, not megabits. On a pipe that thin, over-fetching was not a performance footnote; it was the difference between a playable session and a stuttering mess. You sent the delta. You sent the three fields that changed and nothing else, because every byte you did not need was a byte stealing from the byte you did. You learned to describe precisely what the client required, and the server sent precisely that.

A GraphQL query is that same contract written in a friendlier syntax. The client names the fields; the server returns those and stops. REST hands you the whole product object and lets you throw away nine-tenths of it, which is fine over a fat connection and quietly expensive over a phone on a train. So when I choose GraphQL for a product listing, I am not chasing a trend — I am applying a rule that predates the web: the caller decides what it needs, and nothing rides along uninvited. That is why the pattern feels durable to me rather than fashionable.

What I would watch before you go all-in

Two cautions from experience. First, the flexibility that makes GraphQL efficient is the same flexibility that lets a careless client ship a query five levels deep that hammers your database — which is exactly why the graphql_max_query_depth limit and persisted queries below are not optional niceties on a public endpoint. Lock the query surface down before launch, not after the first traffic spike. Second, do not migrate writes to GraphQL for symmetry’s sake. Cart and checkout carry session and payment state that the REST Store API handles cleanly today; moving them to prove architectural purity buys you risk and no user-facing benefit. Keep the split boring and let each side do the job it is good at.

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