Thesis: A list payload can ship a foreign key without the nested object. UI that maps row.company.hqCity will look empty. Hydrate by id before mapping — or request an expand the API actually supports.
What we shipped first
The people table showed name and blank HQ. Network tab: each row had companyId and company: null. A detail GET to crm.example.com returned the company with the city. The list mapper never fetched it.
The working shape
After the list returns, if company is null and companyId is set, fetch the company (or batch hydrate) before mapping columns. Do not treat a null nest as “no company.”
// List returns companyId with company: null — HQ city is missing until hydrate.
const listRow = {
id: "p_1",
name: "Alex",
companyId: "c_9",
company: null, // nested record omitted on list
};
async function getCompany(id) {
// GET https://crm.example.com/companies/c_9
return { id, name: "Acme", hqCity: "Austin" };
}
async function mapPerson(row) {
const company =
row.company ?? (row.companyId ? await getCompany(row.companyId) : null);
return {
id: row.id,
name: row.name,
hqCity: company?.hqCity ?? null,
};
}
await mapPerson(listRow); // { id: "p_1", name: "Alex", hqCity: "Austin" }
Checklist
- Assert list fixtures where the nest is null and the id is present.
- Hydrate by id (or an explicit expand) before rendering nested fields.
- Keep the same mapper for list and detail so HQ city cannot regress on one path.
Related: the CRM is not the funnel. How we deliver: methodology · readiness.
Engineering commentary only — not audit, legal, or certification advice.