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


Downloaded csv 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 ahasNextPage
flag and anendCursor
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 DefinitionExample 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.
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 ,