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
/graphqlendpoint 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].jsimport { gql } from '@apollo/client';
import client from '../../lib/apollo-client';
const GET_PRODUCT = gql
...; // Query from aboveexport 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_depthto 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
getStaticPropswith 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.
Last modified: August 2, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe