Route → Query → Mapper → UI
A four-layer data architecture that keeps a Next.js frontend stable when the backend API changes.
When a component reads raw API fields directly, the backend owns your UI. Rename one field and several components break; worse, they slowly fill up with defensive code:
<h3>{cat?.name ?? cat?.title ?? cat?.categoryName ?? ""}</h3>In the Aments storefront I used a rule instead: UI components only ever read stable keys. Four layers make that possible.
1. Mapper - the only place that knows the API
const PopularCategoryDefaults = {
id: null, name: "", image: "", items: "(0 Items)", href: "/products",
};
export function mapPopularCategory(raw = {}) {
return {
...PopularCategoryDefaults,
id: raw?.id ?? PopularCategoryDefaults.id,
name: raw?.name ?? PopularCategoryDefaults.name,
image: raw?.image ?? PopularCategoryDefaults.image,
items: `(${Number(raw?.orderCount ?? 0) || 0} Items)`,
href: raw?.id != null
? `/products?categoryId=${encodeURIComponent(String(raw.id))}`
: PopularCategoryDefaults.href,
};
}Defaults live in one object. Derived values - the item count label, the link - are computed here, not in JSX.
2. Query - one function per endpoint
export async function getPopularCategories({ lang } = {}) {
const res = await ApiService.get(STATISTICS_CATEGORY_POPULAR_ROUTE, { params: { lang } });
const list = Array.isArray(res?.data?.data) ? res.data.data : [];
return list.map(mapPopularCategory).filter((x) => x?.id != null && x?.name);
}The query removes the repeated request-extract-map code and drops invalid items before they reach the page.
3. Route - compose on the server
export default async function Page() {
const lang = await getServerLang();
let popularCategories = [];
try {
popularCategories = await getPopularCategories({ lang });
} catch {
popularCategories = [];
}
return <HomePage popularCategories={popularCategories} />;
}A failing endpoint returns an empty list. The section disappears; the page still renders.
4. UI - read stable keys, nothing else
Components receive name, image, items and href. No optional chaining chains, no fallbacks, no knowledge of the backend.
Why it works
This is the anti-corruption layer from domain-driven design, applied to the frontend. The external model (the API) is translated into your own model at the boundary, so a backend change is a one-file change.