The backend-for-frontend: an API shaped for the screen, not the database
A backend-for-frontend (BFF) is a thin server layer that exists to serve one frontend. Its job is not to be a general-purpose API — it is to give this specific client exactly the shape it needs, in as few round-trips as possible. It aggregates several downstream calls into one, reshapes data from database-shaped to screen-shaped, and holds the secrets the browser must not. The problem it solves is the mismatch between how backend services organise data (by domain, normalised, for reuse) and how a screen needs it (denormalised, joined, for one view). Without a BFF the client papers over that gap with a waterfall of calls and a pile of reshaping code; with one, the server does it where it is cheap.
Aggregate the waterfall into one call
The most visible win is collapsing a client-side request waterfall. A dashboard that needs the user, their recent orders, and stock levels would otherwise make three sequential round-trips from the browser (each often waiting on the last). The BFF makes them in parallel, server-side, close to the services, and returns one payload:
// BFF endpoint: one client call fans out to three services in parallel
app.get("/api/dashboard", async (req, res) => {
const [user, orders, stock] = await Promise.all([
userService.get(req.userId),
orderService.recent(req.userId),
inventoryService.levels(),
]);
res.json({ user, orders, stock }); // one response, already joined for the screen
});
The client makes one request over its slow last-mile connection instead of three, and the fan-out happens on fast internal links.
Reshape data for the view, and hold the secrets
The BFF also translates. Backend services return everything, normalised; the screen wants a trimmed, joined, view-specific shape. Doing that in the BFF keeps the client free of reshaping logic and keeps API keys off the browser:
// reshape database-shaped data into screen-shaped data, using a server-only key
const raw = await payments.charges(userId, { key: process.env.STRIPE_KEY }); // secret stays here
res.json(raw.data.map((c) => ({ id: c.id, amount: c.amount / 100, when: c.created })));
The browser receives exactly the fields the component renders — no over-fetching, no client-side money-math, no exposed credential.
One BFF per frontend, and know when not to
The defining discipline is in the name: a BFF serves one frontend. The web app’s BFF and the mobile app’s BFF are allowed to diverge, because their screens have different needs — a shared “do everything” API drifts back into the generic mismatch the BFF was meant to fix. The trade-off is a real extra service to build, deploy, and operate, so a BFF earns its keep when you have multiple downstream services to aggregate, secrets to keep off the client, or a genuine shape mismatch — and is overkill for a single well-designed API that already returns screen-friendly data. Where it fits, it moves the aggregation and reshaping to the side of the network where they are cheap. The proxy-and-cors exercise is the smallest version of this idea — a server layer that stands between your client and an upstream — which is the seed a BFF grows from.