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
/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 |
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: April 3, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe