Get Bundle Product Data
Learn how to fetch bundle product details with component products and images for display in your storefront.
Goal
Fetch complete bundle data including components, images, and inventory for display in your storefront.
Prerequisites
- Published catalog with bundle products
- Authenticated API access
Quick Start
import {
client,
getByContextProduct,
getByContextComponentProductIds,
getStock
} from '@epcc-sdk/sdks-shopper';
// Configure client
client.setConfig({
baseUrl: 'https://euwest.api.elasticpath.com',
headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
});
// Fetch bundle with components
async function getBundle(bundleId: string) {
// Get bundle with component products included
const response = await getByContextProduct({
path: { product_id: bundleId },
query: {
include: ['component_products', 'main_image', 'files']
}
});
const bundle = response.data?.data;
// Verify it's a bundle
if (bundle?.meta?.product_types?.[0] !== 'bundle') {
throw new Error('Product is not a bundle');
}
return {
bundle,
componentProducts: response.included?.component_products || [],
mainImage: response.included?.main_images?.[0]
};
}
Key Concepts
Bundle Type Detection
The bundle type is available at bundle.meta?.product_types?.[0]
. To determine if it's fixed or dynamic, check the component requirements:
// Check if product is a bundle
if (bundle.meta?.product_types?.[0] === 'bundle') {
// Determine if fixed or dynamic based on min/max requirements
const isDynamic = bundle.attributes?.components &&
Object.values(bundle.attributes.components).some(
comp => comp.min != null || comp.max != null
);
}
Fetch Component Configuration
Get detailed component configuration. Both fixed and dynamic bundles have components:
async function fetchBundleComponents(bundleId: string) {
// Get component configuration
const componentsResponse = await getByContextComponentProductIds({
path: { product_id: bundleId }
});
// Structure component data
const componentData = Object.entries(componentsResponse.data || {}).map(
([componentKey, component]) => ({
key: componentKey,
id: component.component_id,
options: component.options.map(opt => ({
productId: opt.id,
quantity: opt.quantity || 1,
type: opt.type,
sortOrder: opt.sort_order || 0
})),
// Include min/max for dynamic bundles
min: component.min,
max: component.max,
name: component.name
})
);
return componentData;
}
Component Product Images
Component product images are not included when fetching a bundle. The component products only contain references to their main_image IDs. To efficiently load all component images, use a batch approach:
import { getAllFiles } from '@epcc-sdk/sdks-shopper';
async function fetchComponentImages(componentProducts: any[]) {
// Extract main image IDs from component products
const mainImageIds = componentProducts
.map(product => product.relationships?.main_image?.data?.id)
.filter((id): id is string => typeof id === 'string');
if (mainImageIds.length === 0) {
return [];
}
// Fetch all images in one batch request
const fileResponse = await getAllFiles({
query: {
filter: `in(id,${mainImageIds.join(',')})`
}
});
return fileResponse.data?.data || [];
}
// Usage with bundle fetch
async function getBundleWithComponentImages(bundleId: string) {
const response = await getByContextProduct({
path: { product_id: bundleId },
query: { include: ['component_products', 'main_image', 'files'] }
});
const bundle = response.data?.data;
const componentProducts = response.included?.component_products || [];
// Fetch component images separately
const componentImages = await fetchComponentImages(componentProducts);
return {
bundle,
componentProducts,
componentImages,
mainImage: response.included?.main_images?.[0]
};
}
Check Inventory
Get inventory data for bundle and components:
async function fetchBundleInventory(
bundleId: string,
componentProductIds: string[],
locationId?: string
) {
// Fetch bundle inventory
const bundleStock = await getStock({
path: { product_uuid: bundleId }
});
// Fetch component inventory
const componentStockPromises = componentProductIds.map(id =>
getStock({ path: { product_uuid: id } })
);
const componentStockResponses = await Promise.allSettled(componentStockPromises);
// Process inventory data
const inventory = {
bundle: processBundleStock(bundleStock.data?.data, locationId),
components: {}
};
componentStockResponses.forEach((result, index) => {
if (result.status === 'fulfilled') {
const productId = componentProductIds[index];
inventory.components[productId] = processBundleStock(
result.value.data?.data,
locationId
);
}
});
return inventory;
}
function processBundleStock(stockData: any, locationId?: string) {
if (!stockData?.attributes) return null;
const attributes = stockData.attributes;
if (locationId && attributes.locations?.[locationId]) {
return {
available: attributes.locations[locationId].available,
allocated: attributes.locations[locationId].allocated,
total: attributes.locations[locationId].total
};
}
return {
available: attributes.available,
allocated: attributes.allocated,
total: attributes.total
};
}
Error Handling
async function safeFetchBundle(bundleId: string) {
try {
const data = await getBundle(bundleId);
return { success: true, data };
} catch (error: any) {
if (error.status === 404) {
return { success: false, error: 'Bundle not found' };
}
if (error.status === 401) {
return { success: false, error: 'Authentication required' };
}
return { success: false, error: 'Failed to load bundle' };
}
}
Display Example
function BundleDisplay({ bundleId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
safeFetchBundle(bundleId).then(result => {
if (result.success) setData(result.data);
setLoading(false);
});
}, [bundleId]);
if (loading) return <div>Loading...</div>;
if (!data) return <div>Bundle not found</div>;
const { bundle, componentProducts } = data;
const isDynamic = bundle.attributes?.components &&
Object.values(bundle.attributes.components).some(
comp => comp.min != null || comp.max != null
);
return (
<>
<h1>{bundle.attributes.name}</h1>
<p>Type: {isDynamic ? 'Dynamic' : 'Fixed'} Bundle</p>
<h2>Components:</h2>
<ul>
{componentProducts.map(product => (
<li key={product.id}>{product.attributes.name}</li>
))}
</ul>
</>
);
}
Performance Tips
- Use includes wisely - Only include data you need
- Batch image fetching - Use
getAllFiles
with filter to fetch all component images in one call - Cache component images - Store fetched images to avoid repeated API calls
- Consider lazy loading - Load images only when components are visible
- Cache bundle structure - Bundle configuration rarely changes
- Implement loading states - Show progress during data fetching
- Handle errors gracefully - Provide fallbacks for missing images
Next Steps
- List Bundle Components - Deep dive into component fetching
- Bundle Pricing Strategies - Handle bundle pricing
- Configure Dynamic Bundles - Build configuration UI