---
title: "How to Implement Product Counts in Shopify Storefront API"
slug: "shopify-storefront-api-product-counts-nextjs"
published: "2025-07-14"
updated: "2025-12-25"
validated: "2025-10-20"
categories:
  - "Shopify"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "unspecified"
status: "stable"
llm-purpose: "A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs."
llm-prereqs:
  - "General familiarity with the article topic"
llm-outputs:
  - "Completed outcome: A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs."
---

**Summary Triples**
- (How to Implement Product Counts in Shopify Storefront API, focuses-on, A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs.)
- (How to Implement Product Counts in Shopify Storefront API, category, general)

### {GOAL}
A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs.

### {PREREQS}
- General familiarity with the article topic

### {STEPS}
1. Follow the detailed walkthrough in the article content below.

<!-- llm:goal="A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs." -->
<!-- llm:prereq="General familiarity with the article topic" -->
<!-- llm:output="Completed outcome: A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs." -->

# How to Implement Product Counts in Shopify Storefront API
> A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs.
Matija Žiberna · 2025-07-14

As a Shopify/Next.js dev, I know how essential it is to show users how many products match their search or filter—seems basic, right? But as I found out building [headless e-commerce with Shopify](https://www.buildwithmatija.com/blog/shopify-headless-vs-liquid-when-to-choose), Shopify's Storefront API has more quirks than you might expect, especially when it comes to product counts on search and collection pages.

Here's a battle-tested guide (plus my personal "aha" moments) for getting accurate product counts with Shopify's Storefront API.

---

## Why Show Product Counts?

Clear product counts make filtering and pagination user-friendly. Customers expect to know “how many red shirts are there?,” and, “how many total products?” Getting that number right also helps with page navigation and lets your UI show helpful filter counts like “Red (4), Blue (7).” 

But here’s where things get tricky, Shopify Storefront API provides product counts differently depending on which query you use.

---

## Types of Queries: Search vs. Collection

After a lot of trial and error, I discovered there are two main ways to get products from Shopify’s API.

- **Search Query:** For searching all products, whether the user is using a global search bar or keyword search.
- **Collection Query:** For browsing products inside a specific collection, like `/collections/jackets`.

Depending on which one you use, you get product counts in totally different ways. 

---

### Use Case 1: Search Query – The Straightforward Way ✅

If you’re building a typical search page, the `search` query is the right tool for the job. Shopify gives you a `totalCount` field, which makes product counts simple and reliable.

#### GraphQL Example:

```graphql
query SearchProducts(
  $query: String!
  $productFilters: [ProductFilter!]
  $first: Int
  $after: String
) {
  search(
    query: $query
    types: PRODUCT
    productFilters: $productFilters
    first: $first
    after: $after
  ) {
    totalCount
    productFilters {
      id
      label
      type
      values {
        id
        label
        count
      }
    }
    edges { node { ... } }
    pageInfo { hasNextPage endCursor }
  }
}
```

#### Example TypeScript Usage:

```typescript
export async function getProducts(params: SearchParams) {
  const response = await shopifyFetch({
    query: getProductsViaSearchQuery,
    variables: {
      query: params.query,
      productFilters: params.filters,
      first: params.first,
      after: params.after,
    },
  });

  return {
    products: response.body.data.search.edges.map((edge) => edge.node),
    pageInfo: response.body.data.search.pageInfo,
    filters: response.body.data.search.productFilters,
    totalCount: response.body.data.search.totalCount,
  };
}
```

#### Developer Note: ProductFilter is Your Best Tool

If you’re not already using `ProductFilter` with the `search` query, I highly recommend switching. I cover this in detail in [this article](https://medium.com/stackademic/how-to-add-product-filters-to-a-headless-shopify-store-with-next-js-15-and-the-storefront-api-42ada0681d92). The short version is: `ProductFilter` lets you request exactly the options (like color, size, etc.) you need. For search queries, the API returns an accurate `totalCount` and counts for each filter value, so your filtering UI can always tell the user “Red (3), Blue (9),” and so on. This makes everything easier for both you and your users.

---

### Use Case 2: Collection Query – The Slightly Tricky Way

Collection pages use the `collection` query. This is where counts can get a little confusing. The response does *not* include a native `totalCount`. That means you’ll have to calculate the full number yourself.

#### Example Collection Query:

```graphql
query GetCollectionProducts(
  $handle: String!
  $productFilters: [ProductFilter!]
  $first: Int
  $after: String
) {
  collection(handle: $handle) {
    products(
      filters: $productFilters
      first: $first
      after: $after
    ) {
      filters {
        id
        label
        type
        values { id label count }
      }
      edges { node { ... } }
      pageInfo { hasNextPage endCursor }
    }
  }
}
```

#### The Confusing Bit

While building my client’s store, I noticed something odd. When searching for products inside a collection, my product count would sometimes be off by just a few—like 2 or 5 products. The total wasn’t *way* off, but it was enough to feel weird. It took me some digging to realize that you have to be careful which filter you use for your totals, and you need to deduplicate some responses. This only happened when searching within a collection, never with global search.

---

#### How I Calculate the Collection Count

First, always deduplicate filter values. Shopify’s response can have duplicates, which will cause the sum to be slightly too high. Then, for accuracy, don’t just sum the first filter; prefer "product type" if available, and "availability" as a backup.

```typescript
export function deduplicateFilters(
  filters: ShopifyFilterResponse[]
): ShopifyFilterResponse[] {
  return filters.map((filter) => {
    const uniqueValues = filter.values.filter(
      (value, index, array) =>
        array.findIndex((v) => v.id === value.id) === index
    );
    return { ...filter, values: uniqueValues };
  });
}

export function calculateTotalFromFilters(
  filters: ShopifyFilterResponse[]
): number {
  const pt = filters.find(f => f.id === "filter.p.product_type");
  if (pt?.values) return pt.values.reduce((sum, v) => sum + v.count, 0);

  const av = filters.find(f => f.id === "filter.v.availability");
  if (av?.values) return av.values.reduce((sum, v) => sum + v.count, 0);

  return 0;
}
```

---

#### Complete Collection Fetch Example

```typescript
export async function getCollectionProducts(params: CollectionParams) {
  const response = await shopifyFetch({
    query: getCollectionProductsQuery,
    variables: {
      handle: params.handle,
      productFilters: params.filters,
      first: params.first,
      after: params.after,
    },
  });

  const rawFilters = response.body.data.collection.products.filters;
  const deduplicatedFilters = deduplicateFilters(rawFilters);
  const totalCount = calculateTotalFromFilters(deduplicatedFilters);

  return {
    products: response.body.data.collection.products.edges.map(
      (edge) => edge.node
    ),
    pageInfo: response.body.data.collection.products.pageInfo,
    filters: deduplicatedFilters,
    totalCount,
  };
}
```

---

## Components for Product Counts

Here’s how I display counts in my Next.js projects:

### Total Products

```tsx
export function ActiveFilters({
  totalCount,
  activeFilters,
  onClearAll,
}: ActiveFiltersProps) {
  return (
    <div className="flex items-center justify-between">
      <span className="text-sm text-neutral-700 font-medium">
        {totalCount} Artikel
      </span>
      {activeFilters.length > 0 && (
        <button onClick={onClearAll} className="text-sm text-blue-600">
          Clear All
        </button>
      )}
    </div>
  );
}
```

### Filter Sidebar

```tsx
export function ProductsFilter({
  filters,
  activeFilters,
  onFilterChange,
}: ProductsFilterProps) {
  return (
    <div className="space-y-6">
      {filters.map((filter) => (
        <div key={filter.id} className="border-b border-gray-200 pb-4">
          <h3 className="font-medium text-gray-900 mb-3">{filter.label}</h3>
          <div className="space-y-2">
            {filter.values.map((value) => (
              <label key={value.id} className="flex items-center">
                <input
                  type="checkbox"
                  checked={activeFilters.some((f) => f.id === value.id)}
                  onChange={() => onFilterChange(filter.id, value)}
                  className="mr-2"
                />
                <span className="text-sm">
                  {value.label}
                  {" "}
                  <span className="text-xs text-neutral-500">
                    ({value.count})
                  </span>
                </span>
              </label>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}
```

### Pagination

```tsx
export function ShopPagination({
  totalCount,
  currentPage,
  pageSize,
  hasNextPage,
  onPageChange,
}: ShopPaginationProps) {
  const totalPages = Math.ceil(totalCount / pageSize);

  return (
    <div className="flex items-center justify-between">
      <span className="text-sm text-gray-700">
        {totalCount} {totalCount === 1 ? "Ergebnis" : "Ergebnisse"}
      </span>
      <div className="flex items-center space-x-2">
        <button
          onClick={() => onPageChange(currentPage - 1)}
          disabled={currentPage === 1}
          className="px-3 py-1 border rounded disabled:opacity-50"
        >
          Previous
        </button>
        <span className="text-sm">
          Page {currentPage} of {totalPages}
        </span>
        <button
          onClick={() => onPageChange(currentPage + 1)}
          disabled={!hasNextPage}
          className="px-3 py-1 border rounded disabled:opacity-50"
        >
          Next
        </button>
      </div>
    </div>
  );
}
```

---

## How It Comes Together

Here’s an example of these components on a collection or search page:

```tsx
export default async function CollectionPage({
  params,
}: { params: { handle: string } }) {
  const { products, filters, totalCount, pageInfo } =
    await getCollectionProducts({
      handle: params.handle,
      filters: [], // Parse from URL params
      first: 12,
    });

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
        {/* Filter Sidebar */}
        <aside className="lg:col-span-1">
          <ProductsFilter
            filters={filters}
            activeFilters={[]} // Parse from URL
            onFilterChange={handleFilterChange}
          />
        </aside>
        {/* Main Content */}
        <main className="lg:col-span-3">
          <ActiveFilters
            totalCount={totalCount}
            activeFilters={[]} // Parse from URL
            onClearAll={handleClearAll}
          />
          {/* Product Grid */}
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
            {products.map((product) => (
              <ProductCard key={product.id} product={product} />
            ))}
          </div>
          <ShopPagination
            totalCount={totalCount}
            currentPage={1}
            pageSize={12}
            hasNextPage={pageInfo.hasNextPage}
            onPageChange={handlePageChange}
          />
        </main>
      </div>
    </div>
  );
}
```

---

## Final Advice and Common Pitfalls

- Use the `search` query and `ProductFilter` when possible. This always gives the most accurate product counts.
- When working with collection queries, calculate the total from filter data, use product type or availability filters, and always deduplicate.
- If your totals are off by a small number, it’s probably a deduplication or variant-counting issue.
- Never add up the variant filters for your total product count (like color, size, etc.), because you’ll get numbers that are slightly too high.
- Cursor-based pagination in Shopify means you can’t jump to pages based on index, so always use the count to drive your display, but stick to what the cursor gives you for navigation.

Once you understand these differences, Shopify product counts become easy and can power great user experiences.

If you get stuck, or want to learn more about product filtering strategies, check out my bigger deep-dive: [How to add product filters to a Headless Shopify store with Next.js 15 and the Storefront API](https://medium.com/stackademic/how-to-add-product-filters-to-a-headless-shopify-store-with-next-js-15-and-the-storefront-api-42ada0681d92).

## LLM Response Snippet
```json
{
  "goal": "A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs.",
  "responses": [
    {
      "question": "What does the article \"How to Implement Product Counts in Shopify Storefront API\" cover?",
      "answer": "A developer’s guide to accurate product counts with Shopify Storefront API. Covers search vs collection queries, filter deduplication, Nextjs."
    }
  ]
}
```