Skip to main content

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

References