Skip to content
Guides

Examples and recipes

Complete queries for listings, product details, seller catalogs, and review pagination.

Last updated:

These recipes show how to compose real reads without replacing operations that remain in REST. Every example uses variables and sends x-Store-Domain.

Collection page

Combine collection metadata with its products in one request. The same $slug feeds both selections:

CollectionPage.graphql
query CollectionPage($slug: String!, $page: Int = 1) {
  collection(slug: $slug) {
    id
    name
    description
    imageUrl
    productCount
  }
  products(
    input: {
      collections: [$slug]
      page: $page
      pageSize: 24
      sort: DEFAULT
    }
  ) {
    pageIndex
    totalPages
    totalCount
    products {
      id
      name
      slug
      imageUrl
      price
      compareAtPrice
      isAvailable
    }
    filters {
      id
      label
      type
      values { value label count selected }
    }
  }
}
Variables
{
  "slug": "summer",
  "page": 1
}

To apply a facet, add values such as option:color:blue or attribute:material:cotton to input.filters. Read products for every limit.

Product page

A PDP normally needs two steps: first resolve the product by slug, then use its opaque ID for related products and reviews. GraphQL cannot use the result of one field as another field's argument within the same operation.

1. Details

ProductPage.graphql
query ProductPage($slug: String!) {
  store { name currency }
  product(slug: $slug) {
    id
    name
    description
    imageUrl
    price
    compareAtPrice
    isAvailable
    images { url name order }
    variants {
      id
      sku
      price
      compareAtPrice
      isAvailable
      options { key name value }
    }
    seller { id name slug }
  }
}

2. Secondary data

Pass the previous product.id as $productId:

ProductSecondary.graphql
query ProductSecondary($productId: String!, $reviewsCursor: String) {
  relatedProducts(productId: $productId, limit: 4) {
    id
    name
    slug
    imageUrl
    price
    isAvailable
  }
  productReviews(
    productId: $productId
    limit: 10
    cursor: $reviewsCursor
    sortBy: "createdOn"
  ) {
    data {
      id
      rating
      title
      content
      reviewerName
      isVerifiedPurchase
      createdOn
    }
    hasNextPage
    nextCursor
    totalCount
  }
}

This design lets you render primary information first and load recommendations or reviews lazily.

Seller catalog

Compose store identity, the seller profile, and its products in one operation:

SellerCatalog.graphql
query SellerCatalog($slug: String!, $pageSize: Int = 12) {
  store {
    id
    name
    currency
  }
  seller(slug: $slug) {
    id
    name
    slug
    logo
    rating { average totalReviews }
    deliveryPolicy
  }
  products(
    input: {
      sellerSlugs: [$slug]
      pageSize: $pageSize
      sort: NEWEST
    }
  ) {
    totalCount
    products {
      id
      name
      slug
      imageUrl
      price
      isAvailable
    }
  }
}
Variables
{
  "slug": "acme",
  "pageSize": 12
}

Paginate reviews

Keep nextCursor as an opaque string. The first request uses null; each following request sends the returned cursor back:

reviews.js
const document = `
  query ReviewPage($productId: String!, $cursor: String) {
    productReviews(
      productId: $productId
      limit: 20
      cursor: $cursor
      sortBy: "createdOn"
    ) {
      data {
        id
        rating
        title
        content
        reviewerName
        createdOn
      }
      hasNextPage
      nextCursor
    }
  }
`;

async function loadReviewPage(productId, cursor = null) {
  const response = await fetch("https://storefront.ecomiq.pe/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-Store-Domain": "my-store",
    },
    body: JSON.stringify({
      query: document,
      variables: { productId, cursor },
    }),
  });

  const result = await response.json();

  if (!response.ok || result.errors?.length) {
    throw new Error(result.errors?.[0]?.message ?? `HTTP ${response.status}`);
  }

  return result.data.productReviews;
}

const firstPage = await loadReviewPage("456");

if (firstPage.hasNextPage) {
  const secondPage = await loadReviewPage("456", firstPage.nextCursor);
  console.log(secondPage.data);
}

Do not increment, decode, or combine the cursor with different filters. When rating, search, or sorting changes, restart pagination with cursor: null.

Query with @ecomiq/storefront

The SDK validates the data object with Zod:

collection-page.ts
import { createClient, z } from "@ecomiq/storefront";

const CollectionPageData = z.object({
  collection: z.object({
    id: z.string(),
    name: z.string(),
    productCount: z.number(),
  }),
  products: z.object({
    totalCount: z.number(),
    products: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
        slug: z.string(),
        price: z.number(),
      }),
    ),
  }),
});

const api = createClient();
const data = await api.graphql(
  `
    query CollectionPage($slug: String!) {
      collection(slug: $slug) { id name productCount }
      products(input: { collections: [$slug], pageSize: 24 }) {
        totalCount
        products { id name slug price }
      }
    }
  `,
  CollectionPageData,
  { slug: "summer" },
);

On this page