Best Practice for Fetching Custom Object Associations in UI Extensions (hubspot.fetch vs. Serverless
Hi HubSpot Developer Team,
I’m building a custom UI card for a custom object page and I need some clarification on the best practice for fetching associated data.
My goal is to display a table of line items. The association path is: Custom Object → Associated Deal → Associated Line Items.
My understanding is that direct API calls from a UI card are blocked by the Content Security Policy (CSP), and the required method for fetching data is to use a serverless function. When I try to call the API directly for my custom object, I get errors as expected.
However, I’m confused because I have another component on a standard Deal page that successfully fetches associated line items directly from the frontend using hubspot.fetch() with a relative API path. This approach works perfectly without a serverless function.
This has made it difficult to understand when a serverless function is truly necessary.
Here is the code that works on a standard Deal card, which is causing my confusion:
import React, { useEffect, useState } from ‘react’;
import { hubspot, Card, CrmAssociationTable } from ‘@hubspot/ui-extensions’;const DealsTimelineCard = ({ context }) => {
const [loading, setLoading] = useState(false);
const [lineItems, setLineItems] = useState([]);useEffect(() => {
const loadProductsForDeal = async () => {
try {
setLoading(true);
const dealId = context?.crm?.objectId;
if (!dealId) {
setLoading(false);
return;
}// This hubspot.fetch() call works directly from the frontend
const assocRes = await hubspot.fetch(`/crm/v4/objects/deals/${dealId}/associations/line_items`);
const assocJson = await assocRes.json();
const ids = (assocJson?.results || []).map(r => r.toObjectId).filter(Boolean);if (!ids.length) {
setLineItems([]);
setLoading(false);
return;
}// This subsequent hubspot.fetch() also works
const batchRes = await hubspot.fetch(`/crm/v3/objects/line_items/batch/read`, {
method: ‘POST’,
headers: { ‘content-type’: ‘application/json’ },
body: JSON.stringify({
properties: [‘name’, ‘quantity’, ‘price’, ‘amount’],
inputs: ids.map(id => ({ id }))
})
});
const batchJson = await batchRes.json();
setLineItems(batchJson?.results || []);} catch (e) {
// Handle error
} finally {
setLoading(false);
}
};
loadProductsForDeal();
}, [context]);return (
<Card>
<CrmAssociationTable
objectTypeId=“0-7” // Line Item Object Type ID
propertyColumns={[‘name‘, ‘quantity‘, ‘price‘]}
searchable={true}
pagination={true}
/>
</Card>
);
};hubspot.extend(({ context }) => <DealsTimelineCard context={context} />);
## My Questions
Could you please clarify the proper method for my use case?
Why does hubspot.fetch() work for fetching associations from a standard Deal object but not for a custom object?
Is there a special, limited proxy for standard objects that allows this? If so, what are its exact capabilities and limitations?
For fetching data related to a custom object, is using a serverless function the only correct and officially supported method?
Understanding the intended architecture here would be incredibly helpful. Thank you!