ProductionGENERALPLATFORM-SPECIFICSIMPLIFIED

Backend for Frontend

A server the frontend team owns, sitting between the browser and several backend services. Why a client asks for one — and what the team signs up for by running it.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

When is a server owned by the frontend team the right answer, and what does that team take on by owning it?

The user intent

Someone wants one screen to load. They do not know or care that the data behind it lives in six services owned by four teams.

The obvious build

The browser calls the services it needs. If a screen needs six, it makes six calls and composes the results in JavaScript.

Why it breaks

Six services means six origins to configure for cross-origin access, six sets of credentials or audiences, and six independent deployment schedules the client is now coupled to (CORS).

How it breaks in a real browser
  • Six services means six origins to configure for cross-origin access, six sets of credentials or audiences, and six independent deployment schedules the client is now coupled to (CORS).
  • The composition happens on the slowest CPU and the worst network in the system. The data centre could have done the same fan-out over links measured in fractions of a millisecond; the browser does it over a mobile uplink (Fan-Out: Waiting for the Slowest of Seven).
  • Some of the calls cannot be made from a browser at all. A third-party API key, a partner credential, a service-to-service token — none of them can live in a bundle, so the call has to happen somewhere else regardless (The Browser Security Model).
  • No individual service wants to return a screen-shaped response, and each of them is right to refuse. A payments service that grows a checkoutPageV3 endpoint has been captured by one client's UI.
  • The client accumulates the aggregation logic, the retry policy, the timeout budget and the partial-failure policy for six dependencies — which is a distributed systems problem being solved in a tab (Retries, and the Duplicate Order).
  • A web app and a mobile app want different shapes of the same data, and a single shared endpoint that serves both becomes a union of two designs that fits neither.
  • Every one of those six contracts must now remain compatible with every client build still running, because the browser is talking to all of them directly (Long-Lived Clients and Version Skew).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • A backend for frontend is a server owned by the team that owns a client experience. It speaks exactly the shape that client needs, and fans out to the services behind it from inside the network where round trips are cheap (Composed APIs: Aggregating Other Services).
  • It moves three specific things off the browser: composition (many calls become one), credential custody (secrets and token exchange happen server-side), and contract adaptation (six upstream shapes become one downstream shape).
  • It commonly becomes the session holder. The browser gets a same-site, HttpOnly cookie it cannot read; the BFF exchanges that for whatever the services actually require. No access token ever reaches JavaScript (Cookies vs Script-Readable Tokens).
  • It is one per client experience, not one shared by all of them. A BFF serving web, iOS, Android and a partner integration has become a shared gateway again, with a contract nobody owns.
  • It is not an API gateway, which applies cross-cutting policy — routing, rate limits, authentication — uniformly for every consumer. A BFF is a consumer with opinions, not a policy layer (The Gateway as Policy Boundary).
  • It is not a microservice: it owns no data and no domain rules. That constraint is the whole governance model, and everything that goes wrong with a BFF starts with it being relaxed (What the Backend Is Responsible For).
  • An SSR server can act as one, and often already does — the request-time layer that renders the page is in the same position and has the same credentials. Whether they are the same process is a deployment decision; that they are the same *position* is worth noticing before building a second one (Server-Side Rendering).

What this makes the browser do

And which of it is avoidable.

  • Substantially less: one origin, one credential, one request per screen, and a payload already shaped for rendering.
  • Less main-thread parse and transform work, because the joining, filtering and reshaping happened upstream (The Real Cost of JavaScript).
  • One authentication flow and one cookie, rather than several token lifetimes to keep straight across tabs (Auth Across Tabs).
  • One extra network hop, which is real: the browser now waits for the BFF, which waits for the slowest service it called (Tail Latency: Why p50 Being Fine Does Not Help).
  • A simpler failure surface — one request to model — and a correspondingly greater need for that one response to describe partial failure honestly.

Why a client asks for one

The request almost never starts as "we would like to run a service". It starts as a screen that needs data from six places, a third-party key that cannot go in a bundle, and a mobile app that wants the same information in a different shape. Each of those has the same solution, and the solution happens to be a server that the client team controls.

What the layer actually does is narrow, and staying inside that boundary is the whole discipline of the pattern. It composes, it shapes, it holds credentials. It does not own data, it does not make product rules, and it does not serve anyone but its own client.

Where the fan-out happens
GET /screens/order-detailexchange, as the useroptional: degrade if slowkey never leaves the serverone screen-shaped response, per-section statusUserBrowser: one request, one origin, one cookieBFF (owned by the frontend team)Session cookie → service tokensThird-party API (needs a secret key)Trace: one request id end to endOrders serviceCatalogue serviceRecommendations
UserLLMAgentToolDataDecisionHumanGuardrail
Do you need a backend for frontend?

A screen needs data from several places and a credential the browser cannot hold. What do you build?

Nothing — fix the contract

when One backend team owns most of the data and is willing to shape responses for you. The cheapest good answer, and the most often skipped (Consumer-First Design).

cost A conversation, a negotiation, and a dependency on someone else's roadmap.

Compose in the client

when Two or three independent calls, no secrets involved, and a client population that is not on poor networks.

cost Round trips on the worst network in the system, and partial-failure handling in every component (How API Shape Drives UI Complexity).

Compose in the existing render server

when Server-side rendering already exists. It is already in the position, already holds credentials, and already runs at request time.

cost Rendering and data composition are coupled in one deployment, and its scaling profile now has two drivers (Server-Side Rendering).

A dedicated BFF per client experience

when Several services behind a screen, several client experiences wanting different shapes, and credentials that must not reach a browser.

cost A production service: deploys, scaling, secrets, monitoring, on-call and a bill — owned by the frontend team (Scoring Operational Complexity).

A shared API gateway with composition

when Many consumers need the same aggregation and the same policy, and no single client owns the shape.

cost The contract belongs to a platform team, so it moves at platform pace and fits no client exactly (API Gateway).

A query layer the client shapes itself

when Client shape requirements change constantly and a single graph over the domain genuinely exists.

cost The cost of an expensive query moves to the server, where a client can now write one by accident (GraphQL: Client-Shaped Queries Over One Schema).

What it must not become

Every failure of this pattern is a boundary violation, and there are only three interesting ones: it acquires logic, it acquires other clients, or it acquires data. Each starts as a reasonable local decision — a small calculation to avoid a round trip, a second app pointed at an endpoint that already exists, a table to cache something expensive — and each converts a thin adaptation layer into an unowned backend service.

It helps to be able to say precisely what a BFF is not, because the neighbouring things look similar on a diagram and behave completely differently in an organisation. The distinguishing question is always ownership: who decides the contract, and who is paged when it fails.

Two things people both call "a BFF"
A pass-through proxy
// "Flexible": the client says what it wants.
app.get('/api/*', async (req, res) => {
  const target = req.query.service + req.path
  const r = await fetch(internal(target), {
    headers: { authorization: SERVICE_TOKEN },  // the BFF\'s own identity
  })
  res.status(r.status).send(await r.text())
})

// This is an authenticated proxy into the internal network,
// calling every service with a broad service credential,
// with the target chosen by the browser.
A composer with a fixed surface
// One route per screen. A fixed set of downstream calls.
app.get('/screens/order-detail/:id', async (req, res) => {
  const user = await session(req)                 // the user\'s identity
  const [order, items, recs] = await Promise.allSettled([
    orders.get(req.params.id, { as: user, timeout: BUDGET.core }),
    catalogue.forOrder(req.params.id, { as: user, timeout: BUDGET.core }),
    recommender.for(user, { timeout: BUDGET.optional }),
  ])

  if (order.status === 'rejected') return res.status(502).json(fail(order))

  res.json({
    order: shape(order.value),
    items: shapeAll(items),
    // optional section degrades instead of failing the screen
    recommendations: recs.status === 'fulfilled'
      ? { status: 'ok', data: shapeAll(recs.value) }
      : { status: 'unavailable' },
    can: capabilities(user, order.value),
  })
})

The right-hand version has a surface you can enumerate, authorize and test; the left-hand version has a surface defined by whatever the client sends. It also distinguishes required sections from optional ones, which is the only way a composed response can degrade rather than fail — and the only way the UI can tell the user something specific (Partial Failure: When 3 of 5 Succeed).

Backend for frontendAPI gatewayDomain serviceSSR render server
Who owns the contractThe one client team it servesA platform team, for all consumersThe team that owns the domainThe frontend team
How many consumersExactly one client experienceEvery consumerEvery service that needs the domainOne application
Owns data?No — neverNoYes, authoritativelyNo
Owns business rules?NoNo — policy onlyYesNo
Primary jobCompose, shape, hold credentialsRoute, authenticate, rate limit, observe (The Gateway as Policy Boundary)Own and enforce a domainProduce HTML at request time
Changes whenThe client's screens changePolicy changesThe domain changesThe UI changes
Failure blast radiusOne client experienceEverythingEverything depending on the domainOne application's pages

What owning one costs

The part of this decision that is consistently under-estimated is not the code. It is that a frontend team acquires a production service in the request path of every screen. That means a deployment pipeline, a scaling story, secret rotation, dependency patching, capacity for the fan-out, a cost line, and someone answering a page at three in the morning when a downstream service starts timing out.

The most important piece of engineering inside it is the latency budget. A BFF that awaits six calls with no deadlines is strictly worse than the browser doing the same thing, because it turns six independent client-side failures into one server-side hang. The budget below is the shape of the answer: a deadline for the whole request, a smaller one per call, and an explicit decision about which sections are allowed to be missing.

What actually goes wrong once you run one
TriggerSymptomCauseResponse
A downstream service slows downEvery screen hangs; the BFF exhausts its connectionsNo per-call deadline and no isolation between dependenciesPer-call timeouts inside a request budget, and separate pools per dependency (Bulkheads).
A downstream service starts failingLatency climbs across unrelated screensEvery request still tries, waits and retriesShort-circuit the failing dependency and serve its section as unavailable (Circuit Breaker).
A second client is pointed at the BFFEndpoints grow optional fields and mode flagsThe one-client boundary was crossed for convenienceA second BFF, or an explicit decision to become a shared layer with shared ownership (API Ownership and the Catalog).
Logic creeps inA pricing rule differs between the app and a reportA calculation was implemented here to save a round tripMove it into the owning service; the BFF composes and shapes, nothing else (Not Leaking Your Internals).
The BFF calls downstream as itselfA user reaches data they should notA broad service credential replaced the user's identityPropagate the user's identity on every call and let the owning service authorize (Object-Level Authorization).
Client-supplied target or id forwarded uncheckedInternal endpoints reachable through your BFFA pass-through surface instead of a fixed oneFixed routes, fixed downstream targets, validated identifiers (Server-Side Request Forgery (SSRF)).
BFF and client deployed independentlyA screen breaks for clients on one build onlySkew between two components that were meant to move togetherDeploy together, or version the screen contract explicitly (Long-Lived Clients and Version Skew).
Response cached to hide latencyOne screen shows sections from different momentsA composed response cached as a unit over sections with different freshnessCache per section with its own lifetime, or do not cache the composition (The Client Cache Model).
Request budget for GET /screens/order-detail
─────────────────────────────────────────────────────────────
  total request deadline ..................... B
    ├─ auth / token exchange (cached) ........ small, must succeed
    ├─ orders.get ............... deadline 0.5B   REQUIRED
    ├─ catalogue.forOrder ....... deadline 0.5B   REQUIRED   (parallel)
    └─ recommender.for .......... deadline 0.2B   OPTIONAL   (parallel)
                                                   |
                    exceeded ──> section marked 'unavailable',
                                 screen still renders

  REQUIRED fails      -> 502 with a structured error the UI can render
  OPTIONAL fails      -> 200 with { status: 'unavailable' } for that section
  budget exhausted    -> return what completed; never wait past B

  Rules that make this work:
    * every downstream call has a deadline; none inherits "no timeout"
    * deadlines are fractions of B, not independent constants
    * a repeatedly failing downstream is short-circuited, not retried
      into a queue of pending requests
    * retries only for idempotent reads, and only inside the budget

How to build it

Most important first.

  • One BFF per client experience, owned by the team that owns that client — including deployment, monitoring and being paged when it breaks. Ownership is not a formality here; it is the reason the thing can move at the client's pace.
  • No business logic and no data of its own. Composition, shaping, credentials, and nothing else. The moment a pricing rule or an authorization decision originates here rather than being carried through, it has become a service that needs a service's discipline (Modular Monolith).
  • Shape responses to screens, and version them with the client. This is the one place a screen-shaped endpoint is cheap, because the same team owns both ends and can change them together.
  • Make partial failure a first-class response shape: per-section status, so the UI can render what arrived and say specifically what did not (Partial Failure: When 3 of 5 Succeed).
  • Keep credentials on the server side of the boundary: third-party keys, token exchange, and an HttpOnly session cookie for the browser (What the Frontend Is Responsible For in Auth).
  • Give every downstream call a timeout and the whole request a budget. A BFF without them converts one slow dependency into a hung screen for everyone (Timeouts).
  • Propagate the user's identity downstream, never the BFF's own. Authorization decisions must still be made by the service that owns the data (Request Context Propagation).
  • Propagate a request id from the browser through the BFF to every service, so one screen's failure is one trace (Correlation IDs: Turning Lines Into a Story).
  • Deploy it with the client so that skew is between two things you own, rather than across six you do not (Deploying a Frontend).
  • Know when not to build one. One backend team already returning good shapes, or an existing SSR layer that can compose, means a BFF is a second system for no new capability.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • A BFF has no direct interface, so the honest framing is what it must not take away. Almost all of it comes down to one thing: the response shape determines how specific the UI is allowed to be, and specificity is what accessible error handling requires.
  • A composed response that collapses six upstream outcomes into one success-or-failure flag removes the client's ability to say "your orders loaded; the recommendations did not". Per-section status is what makes an announceable, accurate message possible at all (Live Regions and Announcement).
  • Capability fields computed in the BFF — where the user's identity and every service's answer are both available — are what let the UI render a disabled control with a reason instead of hiding it. A hidden control is unfindable; a disabled one with an explanation is navigable (Authorization-Aware UI).
  • The BFF decides how long the screen is empty. Waiting for the slowest of six services before responding at all means a screen-reader user has nothing to read for that entire time; responding progressively, or with the fast sections first, is an accessibility decision made on a server (Route Loading Boundaries).
  • Error codes and field paths must survive the hop. A BFF that flattens a structured upstream validation error into a generic message has taken away the client's ability to put the message on the right input (Errors People Can Actually Perceive).

What can go wrong

Failure modes
  • The BFF accretes business logic. It starts as composition, gains a discount calculation "temporarily", and becomes a second backend that no backend team reviews and no frontend engineer wants to own.
  • It becomes shared. A second client is pointed at it because it already exists, then a third, and its contract now belongs to nobody — which is exactly the condition a BFF was meant to fix (API Ownership and the Catalog).
  • It becomes a single point of failure without budgets, timeouts or isolation, so one degraded downstream service takes every screen down at once (Cascading Failure: When the Response to Failure Causes More Failure).
  • The fan-out moved without shrinking: the BFF makes the same per-row calls the browser used to, just on a faster network. Cheaper, still wrong, and now invisible from the client (The N+1 Query Problem).
  • "Just add it to the BFF" becomes the way to avoid every conversation with an API owner, so the underlying contracts never improve and the adaptation layer grows forever (Consumer-Driven Evolution: Telemetry Before Breakage).
  • The frontend team discovers it now has on-call, a scaling problem, a secret rotation schedule and a runtime it is not expert in — none of which was in the estimate.
  • A cache added to the BFF for latency serves a composed response containing sections of different ages, and the inconsistency is impossible to explain to a user (The Client Cache Model).
  • The BFF proxies whatever path the client asks for, in the name of flexibility, and becomes an authenticated open proxy into the internal network (Server-Side Request Forgery (SSRF)).
What can arrive out of order
  • Downstream responses arriving in an order the composition did not expect, so a partial response is assembled from sections of different ages.
  • The BFF and the client deploying separately, producing skew between two components that were supposed to move together (Long-Lived Clients and Version Skew).
  • Concurrent requests each discovering an expired token and all attempting a refresh, unless refresh is coalesced (Session Expiry and the Refresh Race).
  • A cached composition served alongside a freshly fetched section, so one screen shows two different moments in time (Stale-While-Revalidate).
  • A user's permissions changing between the BFF's authorization check and a downstream service's, so one section is authorized and another is not.
Security
  • The BFF is a trust boundary: browser credentials stop here, service credentials start here. That is its most valuable security property and the reason the pattern is often adopted for authentication alone (Trust Boundaries).
  • It lets the browser hold a same-site HttpOnly cookie instead of a token in JavaScript, which removes an entire class of token exfiltration through script injection — while making cross-site request forgery the thing you must now defend against instead (Cross-Site Request Forgery).
  • It must authorize, not merely forward. A BFF that fetches whichever internal URL or record id the client names is a confused deputy with network access to your service mesh (Server-Side Request Forgery (SSRF)).
  • Downstream calls must carry the user's identity. A BFF that calls services with its own broad service credential turns every endpoint it exposes into a potential privilege escalation (Object-Level Authorization).
  • It is a new production system, with secrets, dependencies, a runtime and a patch cadence, owned by a team that may never have run one. That is a real risk and it belongs in the decision (Secrets Management).
  • Collapsing six origins into one simplifies cross-origin configuration. That is an ergonomics win, not a security control — the same-origin policy was never protecting the backend (CORS).
Misreads
  • "A BFF is an API gateway." A gateway applies uniform policy for every consumer; a BFF is one opinionated consumer. Building one and calling it the other is how you get a shared layer nobody owns (API Gateway).
  • "One BFF for all our clients." That is a gateway with extra steps, and the contract immediately stops belonging to any single team.
  • "The BFF is where we put the logic." It is where you put composition, shaping and credentials. Logic there is unowned logic.
  • "It means we do not have to talk to the API teams." It defers that conversation and makes the adaptation layer permanent (Consumer-First Design).
  • "It makes the app faster." It moves a fan-out onto a much cheaper network and adds a hop. Usually a win, occasionally not, and always worth measuring rather than assuming.
  • "SSR is a BFF." An SSR server *can* act as one — same position, same credentials — but rendering HTML and composing an API response are separable concerns that may or may not belong in one process (Client-Side Rendering).
  • "It is just a small proxy." The moment it is in the request path of every screen, it is a tier-one production service with your team's name on the pager.

Measuring it, and what changes in the field

How you would see this
  • Requests per screen from the browser, which is the number the BFF exists to reduce and the cleanest before-and-after (Reading the Browser Waterfall).
  • BFF response time against the slowest downstream call it made. A fan-out is only as fast as its worst member, so the tail of the downstream distribution is the BFF's median (Tail Latency: Why p50 Being Fine Does Not Help).
  • Per-downstream error and timeout rate, and how often each section of a composed response is degraded — the number that tells you whether partial-failure handling is exercised or theoretical.
  • A distributed trace covering browser, BFF and every service, joined by a request id the client generated (Distributed Tracing).
  • A governance metric worth actually tracking: the proportion of the BFF that is composition and shaping versus logic. It only ever moves in one direction unless someone is watching.
  • Cost per screen. A BFF is a service with a bill, and the fan-out it performs is now on your team's budget line (Cost per Service and the Attribution Problem).
Slow device, slow network, large data, old tab
  • With many services behind the screen, the case is strong; with one well-shaped backend it is close to nonexistent.
  • On a high-latency client connection, moving the fan-out into the data centre is worth far more than it is on a fast one — the benefit is proportional to the gap between the two networks.
  • With several distinct client experiences, per-client BFFs are the point. With one client, a BFF is mostly an adaptation layer and may not need to be a separate process (Choosing a Frontend Architecture).
  • If server-side rendering is already in place, that layer is already in this position and already holds credentials; adding a separate BFF should be justified by something the render layer genuinely cannot do (Streaming Server Rendering).
  • In a micro-frontend estate, "one per client experience" needs a definition — per application, or per unit — and getting it wrong produces either a shared BFF or one per team with duplicated composition (Micro Frontends).
What this costs
  • You get one request per screen, server-side credentials and a contract you control. You pay for a production service: deployment, scaling, monitoring, on-call, secrets, patching and a bill.
  • An extra hop is added between the browser and the data. Whether the screen is faster overall depends on whether the fan-out you removed cost more than the hop you added — which is measurable, and should be measured.
  • Per-client BFFs duplicate composition logic across client experiences. That duplication is deliberate: it is what lets each client evolve without negotiating with the others.
  • Deploying the BFF with the client narrows skew to two components and couples two release processes together.
  • A BFF makes it easy to work around a poor upstream contract, which relieves the pain that would otherwise drive the contract to improve (How API Shape Drives UI Complexity).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe pattern — a client-owned composition and credential layer, one per client experience, owning no data — is independent of language, runtime and hosting model. What varies is whether it is a long-running service, a serverless function set, or a route inside an existing render server.
  • PLATFORM-SPECIFICThe operational cost, which is most of the decision, depends heavily on the deployment model: a serverless BFF has near-zero idle cost and introduces cold starts and connection-reuse problems against downstream services, while a long-running one avoids those and requires capacity planning and scaling that a frontend team may never have done (Serverless and Database Connections).
  • SIMPLIFIEDThis lesson treats the BFF as one process for clarity. In practice it is frequently a set of routes inside an existing server-rendering application, and the question "do we need a BFF" is often really "should these routes exist in the render server we already run".

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Distributed Systems — a BFF is a fan-out coordinator, so request budgets, deadline propagation, partial results and the fact that a composed response is only as fast as its slowest member are all the same problems studied there.
  • Software Design — the ownership rule (one consumer, no data, no domain logic) is a module-boundary argument, and every way a BFF decays is a boundary being crossed for a locally reasonable reason.
  • Testing & Reliability Engineering — running a tier-one service in the path of every screen, which is the part of this decision a frontend team is least likely to have costed.