Exporting Shopify Product Data with Metafields to CSV: A Step-by-Step Guide🛍️

Downloaded csv data

Example data

When managing a Shopify store, one frequent request from clients or internal teams is the ability to export product data — including titles, images, and metafields — in a downloadable CSV format, especially for internal reports or external catalog needs.

In this post, I’ll show you how to build a custom functionality using Shopify’s Storefront API, a bit of Admin API (to read metafields data which is not possible alone in storefront API), and JavaScript, so your users can click a button and instantly get a CSV file with the product details.


✨ What We’re Building

The functionality we’re building is triggered by a “Download Product Data” button. When users click this button, the following happens:

API Call and Data Fetching:
The client queries Shopify’s Storefront API to fetch products from the current collection. It uses a GraphQL query to gather:

  • Core product details (ID, title, handle, vendor, tags)

  • The primary product image URL

  • Custom metafields such as “case_price”, “pack_quantity”, “cases_per_layer”, and “cases_per_pallet”

  • Variant data including variant IDs, titles, prices, availability status, and extra metafields (like “unit_price”)

  • Pagination Handling:
    Since a collection may contain many products, the query uses pagination. It loops through the collection using a hasNextPage flag and an endCursor until all products are fetched.

  • CSV Generation and Download:
    After collating all data and arranging the necessary fields into rows, the code converts the information into CSV format and triggers a file download using the Blob API.

In the code snippets below, I’ve provided two versions—one with more comprehensive metafield extraction and headers, and another streamlined version that only fetches selected fields. Both follow a similar logic and show how to use JavaScript’s fetch API with GraphQL for data retrieval.

We’ll create a "Download CSV" button on the storefront that exports:

  • âś… Product Title

  • âś… Featured Image URL

  • âś… Custom Metafields (e.g., “Case Price”, “Pack Quantity”, etc.)

This is especially useful for B2B stores, internal audits, external catalogs, or product marketing sheets.

đź§° Tools & APIs Used

  • Shopify Storefront API (for product title, image, and metafields)

  • Shopify Admin API (for metafield definition setup)

  • Custom Metafield Definitions (with Storefront API access enabled)

  • Vanilla JavaScript for dynamic CSV generation

  • Optional: Use in a Shopify theme or app extension


🪜 Step-by-Step Guide

âś… 1. Create Custom Metafields in Shopify

To store extra data like "Case Price" or "Pack Quantity", you first need to define those metafields:

  • Go to:
    Shopify Admin → Settings → Custom data → Products → Add Definition

  • Example setup:

    • Namespace: custom

    • Key: case_price

    • Content type: Integer, Decimal, or Text

    • Expose to Storefront API: âś… Checked

This enables the data to be accessed via GraphQL on the frontend.

âś… 2. Add a Button to Your Storefront Theme

Open your Shopify theme files (usually collection.liquid or product-grid-item.liquid) and add a button where you want users to export product data:

<button id="download-products-csv" style="padding: 12px 24px; background: #000; color: white; border: none; border-radius: 4px;">
  Download Product Data as CSV
</button>

You can style this with Tailwind CSS, inline styles, or your existing button classes.


âś… 3. Add the JavaScript to Generate the CSV

Now insert the following script just before </body> in your theme.liquid or inside a custom JavaScript file:

document.getElementById('download-products-csv').addEventListener('click', async function () {
  const button = this;
  const btnText = button.querySelector('.btn-text');
  const accessToken = 'a4751ce9429903027f84c48144bc1882'; // Replace this with your token

  const collectionHandle = window.location.pathname.split('/').filter(Boolean)[1];
  let hasNextPage = true;
  let endCursor = null;
  let allProducts = [];

  btnText.textContent = 'Fetching...';
  button.classList.add('progressing');
  button.style.setProperty('--progress-width', '0%');

  try {
    while (hasNextPage) {
    const query = `
      {
        collection(handle: "${collectionHandle}") {
          products(first: 50 ${endCursor ? `, after: "${endCursor}"` : ''}) {
            pageInfo {
              hasNextPage
            }
            edges {
              cursor
              node {
                id
                title
                handle
                vendor
                tags
                images(first: 1) {
                  edges {
                    node {
                      src
                    }
                  }
                }
                metafields(identifiers: [
                  { namespace: "custom", key: "case_price" },
                  { namespace: "custom", key: "pack_quantity" },
                  { namespace: "custom", key: "cases_per_layer" },
                  { namespace: "custom", key: "caser_per_pallet" }
                ]) {
                  key
                  value
                }
                variants(first: 50) {
                  edges {
                    node {
                      id
                      title
                      price {
                        amount
                      }
                      availableForSale
                      image {
                        src
                      }
                      metafields(identifiers: [
                        { namespace: "custom", key: "unit_price" }
                      ]) {
                        key
                        value
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    `;


      const response = await fetch('/api/2023-07/graphql.json', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Shopify-Storefront-Access-Token': accessToken,
        },
        body: JSON.stringify({ query })
      });

      const resData = await response.json();
      const productsEdge = resData.data.collection?.products?.edges || [];


      productsEdge.forEach(edge => {
        allProducts.push(edge.node);
      });

      hasNextPage = resData.data.collection.products.pageInfo.hasNextPage;
      endCursor = productsEdge.at(-1)?.cursor;

      const progressPercent = Math.min((allProducts.length / 250) * 100, 100);
      button.style.setProperty('--progress-width', `${progressPercent}%`);
    }

    if (allProducts.length === 0) {
      btnText.textContent = 'No Products Found';
      return;
    }

    const headers = [
      'No.',
      'Product ID',
      'Product Title',
      'Handle',
      'Variant ID',
      'Variant Title',
      'Price',
      'Case Price',
      'Unit Price',
      'Case Quantity',
      'Cases Per Layer',
      'Cases Per Pallet',
      'Vendor',
      'Tags',
      'Available',
      'Featured Image URL'
    ];



    let rowCounter = 1;
   console.log(allProducts);
    const rows = allProducts.flatMap(p =>
      (p.variants?.edges || []).map(variantEdge => {
        const v = variantEdge.node;

        // extract featured image
        const imageUrl = v.image?.src || p.images?.edges?.[0]?.node?.src || 'No image';
        const casePrice = p.metafields?.find(mf => mf?.key === 'case_price')?.value || 'N/A';
        const caseQuantity = p.metafields?.find(mf => mf?.key === 'pack_quantity')?.value || 'N/A';
        const casePerLayer = p.metafields?.find(mf => mf?.key === 'cases_per_layer')?.value || 'N/A';
        const casePerPallet = p.metafields?.find(mf => mf?.key === 'cases_per_pallet')?.value || 'N/A';
        const unitPrice = v.metafields?.find(mf => mf?.key === 'unit_price')?.value || 'N/A';

        return [
          rowCounter++,
          p.id,
          `"${p.title}"`,
          p.handle,
          v.id,
          `"${v.title}"`,
          `$${v.price?.amount || 'N/A'}`,
          `${casePrice === 'N/A' ? 'N/A' : `${casePrice}`}`,
          `${unitPrice === 'N/A' ? 'N/A' : `$${unitPrice}`}`,
          caseQuantity,
          casePerLayer,
          casePerPallet,
          `"${p.vendor}"`,
          `"${p.tags?.join(', ') || ''}"`,
          v.availableForSale ? 'Yes' : 'No',
          `${imageUrl}`
        ];
      })
    );

    const csvContent = [headers, ...rows].map(e => e.join(',')).join('\n');
    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = 'collection-products.csv';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);

    button.style.setProperty('--progress-width', '100%');
    btnText.textContent = 'Download Complete';
  } catch (error) {
    console.error('Error:', error);
    btnText.textContent = 'Error. Try Again';
  }

  setTimeout(() => {
    btnText.textContent = 'Download Product Data';
    button.style.setProperty('--progress-width', '0%');
    button.classList.remove('progressing');
  }, 2000);
});

Replace "YOUR_STOREFRONT_ACCESS_TOKEN" with your actual Storefront API token, found under:
Shopify Admin → Apps and sales channels → Develop apps → [Your App] → Storefront API

Add the following code to global css file

#download-products-csv {
  position: relative;
  overflow: hidden;
  background-color: #a16bc4;
  color: #fff;
  border: none;
  padding: 10px 20px;
  cursor: pointer;
  min-width:200px;
}

#download-products-csv::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: var(--progress-width, 0%);
  height: 100%;
  background-color: #6e05b4;
  z-index: 0;
  transition: width 0.3s ease;
}

#download-products-csv .btn-text {
  position: relative;
  z-index: 1;
}

âś… 4. Test Your Button on the Storefront

Go to your live storefront, visit a collection page, and click the button. The browser will download a CSV file with all relevant product data, including metafields!

Check the CSV in Excel, Google Sheets, or any spreadsheet app.

Step by step explanation

đź§Ş Bonus Tips

  • You can reuse the same logic to export individual products, all store products, or even variants and inventory levels.

  • Add a loading indicator while the CSV is being generated.

  • Include date-stamping in filenames like product_export_2025-05-24.csv.


📦 Final Thoughts

Letting users or internal staff export product data directly from your Shopify storefront is a small feature with big business value.

  • âś… Internal teams save time creating catalogs

  • âś… B2B buyers get instant access to price lists

  • âś… Developers can automate more workflows with metafields

This approach gives you full control over what gets exported — without any backend complexity or third-party apps.

A Note on GraphQL vs. REST

Although this example uses GraphQL (via the Storefront API) to retrieve data efficiently—especially for nested resources like metafields and variants—a similar approach can be applied using REST calls. The primary advantage of GraphQL here is that it allows you to fetch exactly the fields you need in one call, reducing the number of HTTP requests. However, if you prefer using REST, you’d follow similar steps:

  • Make a fetch call with the REST endpoint,

  • Iterate over paginated results,

  • Collate and format the data into CSV.

0
Subscribe to my newsletter

Read articles from Rakeshraj Mahakud directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rakeshraj Mahakud
Rakeshraj Mahakud

I am a software developer ,