Skip to the content.

The backend-for-frontend: an API shaped for the screen, not the database

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.

A BFF aggregates several services into one screen-shaped response The client makes one request to the BFF, which fans out to user, orders and inventory services in parallel and returns a single combined, reshaped payload. client 1 request BFF user service orders service inventory service fan out in parallel, combine, reshape
One client request; the BFF fans out to the services the screen needs, in parallel, and returns a single response already shaped for that screen.

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.