When I look at an architecture diagram, the box between React and the backend can seem like an easy thing to remove. Another deployment. Another network hop. Another place for a request to fail.
Those costs are real. But counting boxes misses a more useful question: which decisions would the frontend inherit if that boundary disappeared?
A Backend for Frontend earns its place when it gives the interface a contract shaped around the experience, rather than making the browser coordinate the backend's structure. Sam Newman's explanation of the pattern connects that boundary to a particular user experience and the team responsible for it.
The useful measure is how much complexity it removes from everyday product work.
Start with the decisions leaking into React
Imagine a device-management dashboard. This is a hypothetical example, not a production case study. The screen needs a device list, the latest readings, active alerts, and the actions available to the signed-in user.
The backend might organize those concerns into inventory, telemetry, alerts, and identity services. That organization may be sensible for the backend. It doesn't necessarily match the screen.
With direct calls, the frontend may need to join identifiers, reconcile different timestamps, translate upstream errors, and decide what happens when telemetry fails but inventory succeeds. Eventually a hook named useDevices starts carrying the knowledge of several backend systems.
That is the seam I would inspect before proposing a BFF. Moving those decisions server-side can give React a stable view of the product, even when the upstream services evolve separately.
Shape the response around a usable screen
For this dashboard, I would start with an endpoint such as GET /api/device-overview. The BFF gathers the required data and returns an explicit contract:
This is an illustrative response shape, not a complete implementation. Its important feature is that missing telemetry has a meaning. null doesn't silently become zero, and the timestamp lets the interface communicate freshness.
React still owns presentation: loading feedback, sorting interactions, layout, and accessible status messages. The BFF owns the translation from upstream data into this contract. Authoritative business rules stay with the domain services.
For example, inventory failure might prevent the overview from rendering. Telemetry failure could leave the device list usable with an unavailable-reading message. That is a product decision to confirm, not something a generic catch block should invent.
Put the trust boundary on the server
In this example, the browser authenticates to the BFF using a server-managed session cookie. Upstream credentials stay server-side. React doesn't need service keys or the details of token refresh.
HttpOnly prevents JavaScript from reading the cookie, and Secure limits its transmission to HTTPS. SameSite influences cross-site cookie sending. Those properties have distinct jobs; a cookie configuration alone isn't a complete security design. See MDN's Set-Cookie reference.
The server still needs session expiry and revocation, request validation, and appropriate CSRF protection for cookie-authenticated mutations. Tenant scope must come from authenticated context and verified membership, rather than trusting a supplied tenant identifier.
canRestart helps render the interface. It grants no permission. Every restart request must be authorized against the current user, tenant, and device, with enforcement at the trusted service boundary. Frontend Authorization Is Not Security covers that distinction in more depth.
Keep commands and live updates distinct
The initial overview can arrive through HTTP. A restart command can use a normal HTTP mutation. Live readings can arrive through an authorized real-time service, using SSE or WebSockets when the interaction warrants it.
The live connection needs its own verified identity and tenant scope, plus a plan for expiry, access changes, and reconnects. The client also needs to reconcile a snapshot with later events; the diagram doesn't solve ordering or missed updates.
The BFF can handle streaming where its runtime supports it, but a separate real-time path may fit operational constraints better. Next.js documents deployment limits, including restrictions on long-lived connections in some hosts. Real-Time Doesn't Mean Everything Needs a WebSocket explains why transport should follow the product requirement.
A gateway and GraphQL answer different questions
An API gateway often provides shared routing, traffic limits, and perimeter controls. A BFF focuses on the needs of a particular interface. They can coexist, and their capabilities can overlap: gateway aggregation can also combine upstream calls. The distinction is responsibility and ownership, not whether either box can forward JSON.
GraphQL lets clients specify the fields they need. A suitable schema and resolvers might already provide the composition the screen requires. GraphQL can also be the BFF's interface; choosing it doesn't settle who owns session handling, authorization, failure behavior, or the product contract.
Before adding another service, check whether the existing API can meet the requirement cleanly.
Make the operational cost explicit
A BFF adds a failure boundary and a latency budget. Parallel upstream calls can reduce unnecessary waiting, but fan-out still consumes capacity and can multiply pressure during retries.
I would define deadlines, bounded retry behavior, cancellation, and upstream rate limits alongside the response contract. Retrying a command requires care about duplicate effects. Logs and tracing should identify the dependency that failed without exposing credentials or sensitive payloads.
Caching also needs a deliberate policy. Tenant-specific and permission-sensitive responses must not leak between users. Freshness expectations should be explicit, and authorization changes must not be hidden behind stale cached capabilities.
Keep the layer narrow. If it starts owning billing rules, device lifecycle policy, and durable workflows, inspect whether it is becoming a second domain backend. Frontend ownership of the BFF means owning its operations too.
The checklist I would use
Introduce a BFF when the answers justify the cost:
- Which repeated orchestration or translation decisions leave the browser?
- Does the proposed contract express the screen's actual needs and failure states?
- Which credentials and session responsibilities belong server-side?
- Who owns resource authorization, tenant isolation, and freshness?
- Can the team operate the layer within its latency and reliability budget?
- Could an existing endpoint or GraphQL resolver solve the problem more simply?
I would keep direct API access when a browser-safe, well-shaped API already serves the experience and the extra layer would mostly forward requests. Microsoft's BFF guidance makes the additional deployment and latency costs explicit.
The architectural work is making those tradeoffs visible. A BFF is useful when it gives the frontend fewer backend decisions to carry, and a clearer contract to verify.
