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​

Ask External AI