What a backend for frontend is and the problem it solves

A backend for frontend (BFF) is a server-side component built for one specific client experience, such as the iOS app, the web dashboard, or a partner integration. It sits between that client and the downstream domain services. It calls whichever services a screen needs, combines their results, and returns a response shaped to what the client renders.

The pattern addresses the friction that comes from serving every client through one general-purpose API. Different clients need different data. A desktop web page may show a full order history with line items, while a mobile list view needs only order IDs, totals and status. A shared endpoint ends up sending too much to one client and too little to another. The client that gets too little has to make follow-up calls to fill the gaps. Every change to the shared contract also has to be negotiated across all consuming teams, so each client team ships on the slowest team's cadence.

Mobile is the classic motivation. A phone on a cellular network pays a high latency cost for every request. If rendering a home screen takes five sequential API calls, the user waits through five round trips over that link. A BFF moves the fan-out to the data-center side, where calls between services run over a fast internal network. The device makes one request and receives one compact payload.

The BFF also gives you a place to absorb change. Installed app versions can't be force-upgraded on your schedule. The BFF can keep serving older response shapes to those versions while domain services evolve underneath.

How a BFF fits alongside an API gateway

A BFF does not replace the API gateway. In most deployments the request path runs client → gateway → BFF → domain services. Each hop should own a distinct job.

The gateway handles concerns that apply to all traffic: TLS termination, coarse authentication (is this a valid token or session at all?), rate limiting per client or IP, and routing to the right upstream. It should stay generic, because every team depends on it and changes to it are risky. The BFF sits behind it and does work specific to one client: calling several services, merging their results, trimming fields, and adapting errors into something the UI can render.

Concern API gateway BFF
TLS termination Yes No
Coarse authentication Yes Validates session
Rate limiting Global, per client Rarely; only client-specific limits
Routing To BFFs and public APIs To domain services
Response composition and shaping No Yes
Business authorization No No (enforced in domain services)

The BFF also has a security role for browser clients. It can run the OAuth flow server-side, keep access and refresh tokens out of JavaScript, and give the browser only an HttpOnly, Secure session cookie. That reduces token theft through XSS. Authorization still belongs in the downstream services, and each one should check what the caller may do instead of trusting the BFF's judgement.

Write these boundaries down. When they are vague, teams either duplicate auth checks and rate limits at both layers, or each layer assumes the other handles them and nobody does.

+-----------------------+       +-----------------------+
| Browser (SPA)         |       | Mobile app            |
| session cookie only   |       | own access token      |
| (HttpOnly, Secure)    |       |                       |
+-----------+-----------+       +-----------+-----------+
            | HTTPS                         | HTTPS
            v                               v
+--------------------------------------------------------------+
| API GATEWAY (shared, generic)                                |
|  TLS termination | coarse authn | rate limits | routing      |
+-----------+-------------------------------+------------------+
            |                               |
            v                               v
+-----------------------+       +-----------------------+
| Web BFF               |       | Mobile BFF            |
|  validate session     |       |  validate token       |
|  run OAuth, hold      |       |  compose + shape      |
|  tokens server-side   |       |  trim fields          |
|  compose + shape      |       |  map errors for UI    |
+-----------+-----------+       +-----------+-----------+
            | caller identity               |
            +---------------+---------------+
                            |
        +-------------------+-------------------+
        v                   v                   v
+---------------+   +---------------+   +---------------+
| Orders svc    |   | Users svc     |   | Catalog svc   |
| business      |   | business      |   | business      |
| authz checks  |   | authz checks  |   | authz checks  |
+---------------+   +---------------+   +---------------+

Requests pass through a shared gateway that handles TLS, coarse authentication, rate limiting and routing, then to a per-client BFF that holds OAuth tokens server-side and shapes responses, while each domain service still enforces its own business authorization.

What belongs in a BFF and what stays in domain services

The same discipline applies one level down, between the BFF and the domain services behind it. A useful test is whether the logic would still be correct if a second client called the domain service directly. If the answer is no, the logic is probably a business rule and belongs downstream. If the logic only exists to suit one screen, it belongs in the BFF.

Concern BFF Domain services
Aggregating several calls into one response Yes No
Field selection and response shaping Yes No
Format translation (dates, units, enums to labels) Yes No
Client-specific caching Yes No
Session and token handling for the client Yes No
Domain rules (pricing, eligibility, validation) No Yes
Writes that need consistency across services No Yes
Data ownership and the system of record No Yes

The table's rows tend to be crossed in small, reasonable-looking steps. Someone adds a discount check to the mobile BFF because the domain team's backlog is full. A month later the web BFF gets a similar check, written by someone else with slightly different rounding. The two clients now quote different prices for the same cart. The order service no longer describes what customers actually see.

That is logic creep. The cost is more than duplicated code. Behaviour now diverges between clients, and the domain services stop being the source of truth. Debugging a pricing complaint then means reading every BFF.

Cross-service writes carry a similar risk. If a BFF coordinates them, a partial failure can leave data inconsistent, and no single team owns the cleanup.

Who should own a BFF and how many to build

A BFF exists to serve one frontend, so the team that builds that frontend should own it. They should write it, deploy it on their own schedule, and carry the pager for it. This is the team-autonomy force described in the microservice architecture pattern: a team must be able to develop, test and deploy its software without waiting on other teams. If a mobile screen needs a new field, the mobile team should be able to add it to the BFF and ship both changes together.

The common failure is giving every BFF to a central backend team. That team now sits between every frontend change and production. It has to prioritise requests from web, mobile and partner teams against each other, and it often knows less about the screens than the people asking. The BFF becomes the queue that the pattern was meant to remove.

How many BFFs to build

Build one BFF per client experience, not one per device type. iOS and Android apps usually share screens, flows and data needs, so a single mobile BFF serves both. A web app with denser pages and different navigation gets its own. The deciding factor is how much the experiences overlap. If two clients would make nearly identical calls, splitting them only duplicates code.

Do not create a BFF for each platform, app version or partner. Handle versions inside a BFF, and give partners a stable public API instead. Splitting BFFs is easy. Merging them later is expensive, because each one has picked up its own behaviour, consumers and deployment pipeline.

Implementing aggregation safely

Take a mobile order-detail screen. The handler needs the order, its shipment status and product recommendations. No call depends on another's result, so it issues all three in parallel. Total latency then tracks the slowest call rather than the sum of all three.

Each call gets its own timeout, set below the handler's overall deadline. The order is critical: if it fails, the handler returns an error. Recommendations are optional. On timeout or error, the handler returns an empty list and a flag the client uses to hide that section. The response is trimmed to the fields the screen renders. Every outgoing request carries the incoming trace context, such as the W3C traceparent header, so a slow screen can be traced to the dependency that caused it.

Fan-out amplification

One client request becomes several downstream requests. If recommendations slows down, every in-flight screen load holds a connection and a worker while it waits. The BFF can then run out of threads or sockets, which also breaks endpoints that never call recommendations. Each control addresses a different part of this:

  • Timeouts cap how long each wait lasts.
  • Bulkheads are separate connection pools or concurrency limits per dependency. They stop one slow service from using capacity meant for the others.
  • Circuit breakers stop calls to a failing dependency, so the client gets the fallback immediately instead of after a timeout.

Old app versions

Mobile users update slowly, so the BFF must keep returning the response shapes that installed versions expect. Track which app versions still send traffic. Keep the version-specific mapping in a thin layer at the edge of the handler, and delete it once that traffic stops.

// Order-detail BFF handler: parallel fan-out, per-call timeouts, partial response.
// Node 18+ (global fetch, AbortSignal.timeout). Hostnames are placeholders.
const ORDERS = "";
const SHIPPING = "";
const RECS = "";

async function getJson(url: string, timeoutMs: number, traceparent?: string) {
  // Forward W3C trace context so each downstream span joins the client's trace.
  const headers: Record<string, string> = traceparent ? { traceparent } : {};
  // Per-call timeout, set below the handler's overall deadline.
  // In production, wrap this per dependency with a bulkhead and a circuit breaker.
  const res = await fetch(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
  if (!res.ok) throw new Error(`${url} -> HTTP ${res.status}`);
  return res.json();
}

export async function orderDetail(orderId: string, traceparent?: string) {
  const id = encodeURIComponent(orderId);
  // Independent calls start together, so latency tracks the slowest call, not the sum.
  const [order, shipment, recs] = await Promise.allSettled([
    getJson(`${ORDERS}/orders/${id}`, 800, traceparent),
    getJson(`${SHIPPING}/shipments?orderId=${id}`, 500, traceparent),
    getJson(`${RECS}/recommendations?orderId=${id}`, 300, traceparent),
  ]);

  // The order is critical. Without it there is no screen, so surface the error.
  if (order.status === "rejected") throw order.reason;
  const o = order.value;

  return {
    // Trim to the fields the screen actually renders.
    id: o.id,
    total: o.total,
    items: o.items.map((i: any) => ({ sku: i.sku, name: i.name, qty: i.qty })),
    shipmentStatus: shipment.status === "fulfilled" ? shipment.value.status : null,
    // Optional section: return an empty list plus a flag the client uses to hide it.
    recommendations: recs.status === "fulfilled" ? recs.value.items.slice(0, 5) : [],
    recommendationsAvailable: recs.status === "fulfilled",
  };
}

A TypeScript BFF handler that runs three independent downstream calls in parallel, gives each call its own timeout, fails on the critical order call, falls back on optional data, trims the response and forwards the W3C traceparent header.

When to use a BFF, when to avoid it, and the alternatives

A BFF earns its cost when two conditions hold together. First, the clients need differently shaped data, such as a mobile API that needs compact, pre-joined payloads for slow networks while a web dashboard needs dense tables and bulk filters. Second, different teams own those clients. Without the ownership split, a BFF adds a network hop and a deployable without giving any team more autonomy. Without the data split, you end up with several copies of the same endpoints that drift apart.

Skip it when you have a single client, because the general API already serves that client. Skip it for a small team, where one more service to deploy, monitor and patch costs more than the coordination it saves. Skip it when clients have nearly identical data needs, since per-client backends would duplicate logic with no benefit.

Consider these alternatives first:

Option Fit Ownership Operational overhead
Per-client BFF Divergent client experiences, separate client teams Each client team owns its BFF High: one service per client
Well-designed general-purpose API Similar clients, stable needs Domain or platform team Low: no extra tier
Query layer with field selection Many clients needing different subsets of the same data Platform team owns schema and resolvers Medium: query cost limits, caching is harder
Gateway response transformation Small reshaping such as renaming or removing fields Gateway or platform team Low to medium, but logic in gateway config is hard to test

Gateway transforms work for trimming and renaming fields. Once you need aggregation or client-specific logic, treat that as a sign the work belongs in a BFF.

Impact on development cost and engineering time

A BFF adds a network hop and a failure point between the client and domain services, so its latency and availability now limit every screen it serves. It is one more service to deploy, monitor, scale and carry on call. Duplication is the cost teams tend to underestimate: pagination, error mapping and auth glue get rewritten in each BFF and slowly drift apart. A shared library reduces that duplication but couples teams again, because each library release becomes a coordinated upgrade.

Costs Savings
Extra hop, extra failure point Frontend teams ship without waiting on a shared API release
Deploy, monitor, scale, on-call per BFF Aggregation moves server-side, so client code gets simpler
Duplicated logic that drifts between BFFs Fewer cross-team negotiations over response shapes

Illustrative estimate (assumed numbers, not a benchmark)

Coordination hours per quarter = shape changes × parties per change × hours per party

Assume three client teams, each requesting 10 response-shape changes per quarter, for 30 changes in total.

  • Without a BFF: each change involves the requesting team, the API team and one other client team checking for breakage. At 2 hours per party, 30 × 3 × 2 = 180 hours.
  • With a BFF: assume 80% of changes stay inside the owning team's BFF. The remaining 6 changes need the domain team, which gives 6 × 2 parties × 2 hours = 24 hours. Add the BFF operating overhead at an assumed 2 hours per week, per BFF, over 13 weeks: 3 × 2 × 13 = 78 hours. The total is 102 hours.

The BFF only comes out ahead when shape changes are frequent. If they are rare, the fixed operating overhead dominates.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to backend for frontend pattern, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Changing a shared variable is one edit and one review instead of editing every service's .env by hand.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.