BFF, REST & GraphQL — Owning the Client Data Layer

The BFF is the layer a frontend Lead actually owns. But "BFF vs GraphQL" is a false choice that trips up interviews — they operate at different levels. This lesson covers: what a BFF is, what REST and GraphQL BFFs each look like, when to reach for which, and how they connect to GraphQL Federation. The goal is a single, confident answer that holds up to pushback.

1 The problem: a chatty client talking to many services

In a microservices world there is no single "backend" — there are dozens of small services (search, pricing, reviews, availability, loyalty…). If the browser talks to them directly, three pains appear, and they're the exact pains you've already met under other names:

Hotel detail page — direct vs BFF (interactive)
Press ▶ Play or Next › to walk through the request flow step by step.
0ms 100 200 300 400 500 600 700 800ms
❌ Direct — 5 sequential client round trips
1 /hotel
GET /hotel · 180ms
2 /pricing
waits → GET /pricing · 200ms
3 /reviews
waits → GET /reviews · 160ms
4 /avail
waits → GET /avail · 140ms
5 /loyalty
waits → GET /loyalty · 120ms
✓ Via BFF — 1 browser round trip, parallel datacenter fan-out
Browser
→ BFF
→ Browser ✓
1 /hotel
internal · 180ms
2 /pricing
internal · 200ms
3 /reviews
internal · 160ms
4 /avail
internal · 140ms
5 /loyalty
internal · 120ms
800ms Direct · 5 sequential round trips
260ms Via BFF · 1 round trip · 3× faster

2 What a BFF actually is

A Backend for Frontend is a thin server-side layer that exists to serve one specific frontend experience. It sits between your UI and the microservices, calls them on the UI's behalf (over the fast internal network), and returns a single response shaped exactly for that screen. The pattern came out of SoundCloudPhil Calçado and Sam Newman named it in 2015 — when one monolithic API couldn't serve a web UI and a mobile app whose needs sharply differed.

Two words carry the whole definition, and they're what separates it from a generic gateway:

"for Frontend"

One BFF per UX, not one for everyone

It's coupled to a particular client on purpose. The mobile app's needs (tiny payloads, fewer round trips) differ from the web app's, so each gets its own BFF tuned to it. Coupling that would be a sin between services is the point here.

"Backend" — owned by the FE team

The org signal that matters

The team that builds the UI builds and owns its BFF. That's what gives the frontend team the autonomy to reshape its own API without filing a ticket against a backend team and waiting a sprint.

One-liner

A BFF is one backend per frontend experience, owned by the frontend team. It exists to serve this UI — not to be a general-purpose API.

3 BFF vs API Gateway vs direct-to-services

These get conflated constantly — distinguishing them cleanly is a quick credibility win. A BFF is a kind of API gateway, but a specialised one: one per client experience, rather than one shared edge for everybody.

ApproachWhat it isBest when
Direct to servicesBrowser calls each microservice itselfOne simple client, few services. Waterfalls + coupling appear fast.
API GatewayOne shared entry point for all clients — global concerns: routing, rate-limit, authN, TLSYou need cross-cutting edge policy. But "one size fits all clients" can't optimise any single UI.
BFFA dedicated backend per frontend, shaping responses for that UIMultiple UIs with divergent needs (web vs mobile); the FE team wants to own its contract.

They're not exclusive — a common production layout is a shared gateway for global concerns in front of per-client BFFs (Microsoft Azure Architecture Center).

4 What the BFF does for you — five jobs

  1. Aggregate — fan out to N services in parallel inside the datacenter (sub-millisecond hops) and return one payload. Kills the client waterfall.
  2. Shape & trim — return exactly the fields this screen needs, in the structure it wants. No over-fetching, less client-side transformation.
  3. Orchestrate — encapsulate "call A, then B with A's result" sequencing on the fast internal network instead of across the public internet.
  4. Secure — the BFF holds secrets and talks to services with privileged credentials; the browser never sees them. This is the token-handler pattern: the opaque/refresh token lives server-side in the BFF, the browser only gets an HttpOnly cookie (ties directly to L18's XSS-vs-CSRF storage triangle).
  5. Degrade gracefully — when one downstream service 503s, the BFF decides the fallback (return the hotel card minus price) so the UI gets a coherent partial response instead of orchestrating failure itself.
// One call from the browser → parallel fan-out server-side → one shaped response
app.get('/bff/hotel-page/:id', async (req, res) => {
  const [hotel, price, reviews] = await Promise.allSettled([   // parallel, not waterfall
    getHotel(req.params.id),
    getPrice(req.params.id, req.user),   // privileged creds stay here — never reach the browser
    getReviews(req.params.id),
  ]);
  res.json({
    name:   hotel.value.name,
    price:  price.status === 'fulfilled' ? price.value : null,  // degrade, don't fail
    rating: reviews.value?.avg,
  });
});

5 REST BFF vs GraphQL BFF — same topology, different contract

Now that you know what a BFF does, the question that trips up interviews: "Why BFF instead of GraphQL?" The correct answer is that this is a false choice — they operate at different levels:

BFF = topology

Where the layer sits and who owns it

A server-side aggregation layer, owned by the FE team. Nothing about the topology specifies how the browser talks to it.

REST / GraphQL = contract

How the browser expresses what it needs

REST: named HTTP endpoints (GET /bff/hotel-page/:id). GraphQL: one endpoint, flexible queries. The BFF does the same parallel fan-out behind either contract.

One-liner

BFF is a topology; REST and GraphQL are contracts. A GraphQL server that aggregates microservices for one FE team is a BFF with a GraphQL interface — they're not alternatives, and you choose them independently.

The five BFF jobs don't change. What changes is how the browser expresses what it needs and how the contract is typed:

REST BFFGraphQL BFF
RequestGET /bff/hotel-page/:idquery { hotel(id:$id){ name price } }
Field selectionServer decides — one fixed shape per endpointClient decides per query — zero over-fetch by design
New data needNew endpoint (or query params)Add fields to the query; no new endpoint
HTTP cachingNative GET caching, CDN-friendly out of the boxHarder — POST by default; needs persisted queries
Type systemOpenAPI / manual contractSDL introspection, auto-generated TS types
N+1 riskControlled at the endpoint levelResolver-level — requires DataLoader / batching
Debuggingcurl a URL in productionGraphiQL / Apollo Sandbox — richer but more setup
Learning curveLow — standard HTTPHigher — schema design, resolvers, DataLoader
// REST BFF — fixed shape per screen, server decides fields
app.get('/bff/hotel-page/:id', async (req, res) => {
  const [hotel, price, reviews] = await Promise.allSettled([
    getHotel(req.params.id), getPrice(req.params.id, req.user), getReviews(req.params.id)
  ]);
  res.json({ name: hotel.value.name, price: price.status==='fulfilled' ? price.value : null, rating: reviews.value?.avg });
});

// GraphQL BFF — identical fan-out, but the client picks which fields it needs
const resolvers = {
  Query: {
    hotel: async (_, { id }, { user }) => {
      const [hotel, price, reviews] = await Promise.allSettled([
        getHotel(id), getPrice(id, user), getReviews(id)
      ]);
      return { name: hotel.value.name, price: price.value, rating: reviews.value?.avg };
    }
  }
};
// Detail page: query HotelPage($id:ID!){ hotel(id:$id){ name, price, rating } }
// List card:   query HotelCard($id:ID!){ hotel(id:$id){ name, price } }
// Same BFF, same fan-out — client declares exactly what it needs per call

6 What to reach for first — and the upgrade triggers

The question isn't "BFF or GraphQL" — it's which contract on top of your BFF topology, and when does the topology itself need to change?

Start with REST BFF when…

Upgrade the contract to GraphQL BFF when…

Move to GraphQL Federation when…

The progression isn't always linear. Many teams go REST BFF → Federation directly — they hit sprawl before field-selection pain. SoundCloud (who popularised BFF) went straight to Federation: ~86% less compute, ~45% better latency (The New Stack). Shopify runs a GraphQL BFF. Netflix runs Federation. The trigger is the pain you actually feel, not a prescribed sequence. Don't add GraphQL before the field-proliferation hurts; don't add Federation before you have sprawl.

7 The modern angle: your SSR server is a BFF

With SSR and React Server Components (L09), the framework's server layer already plays the BFF role: a Next.js Route Handler, a server action, or an RSC fetching data on the server is doing exactly the BFF's five jobs — aggregating services, holding secrets, shaping the response, and owning degradation. RSC takes it furthest: the component fetches and renders on the server, so the data-shaping layer and the UI that consumes it are the same code, and zero aggregation JS reaches the browser.

This means: in a Next.js stack, you don't need a separate BFF tier for the web surface. You'd only add a dedicated BFF for a client that doesn't go through that server — a native app, a partner API, a CLI tool.

One-liner

If you're doing SSR or RSC, you already have a BFF — your server layer. Add a dedicated BFF tier only for clients that don't run through that server.

8 The costs — and keeping it thin

A BFF is not free. The Lead-level answer names the downsides without being asked:

9 Platform Lead synthesis — the full answer

How to land all of this in the room in one confident answer:

"I'd put a BFF per surface — web and native have very different payload budgets. It collapses the hotel-page waterfall into one datacenter-side fan-out, trims responses to what each card needs, holds the auth tokens server-side so nothing sensitive reaches the browser, and owns graceful degradation when pricing flakes. In our Next.js stack the Route Handlers and Server Components are that BFF — no separate tier needed for web. I'd start with a REST contract: screen shapes are stable, GET endpoints are CDN-cacheable, and we ship fast. I'd switch to a GraphQL contract when field-selection proliferation kicks in. And if we hit BFF sprawl across many surfaces, that's the specific trigger for GraphQL Federation — keeping each domain team's subgraph independent. The key thing: BFF and GraphQL aren't alternatives; GraphQL is just a contract you put on top of the BFF topology."

Full loop

Concept: a BFF is one thin, FE-owned API per surface — it collapses the client's request waterfall into a single datacenter-side fan-out, trims each response to what that surface needs, and holds auth tokens server-side; in a Next.js stack the Route Handlers + Server Components are that BFF. Trade-off: it's another tier to run and "BFF down = UI down," so you own its availability and keep it thin (orchestration only, no business logic) — and you pick the contract by pain: REST first (stable screen shapes, CDN-cacheable GETs, fast ship), GraphQL when field-selection proliferation bites, Federation only once you hit BFF sprawl. Anchor: "On the hotel page we replaced a 5-hop client waterfall (~800ms) with one BFF fan-out (~260ms) — 3× faster on WiFi, ~400–500ms saved on SEA 4G — by trimming payloads per card and holding tokens server-side; we stayed on REST until field proliferation pushed one surface to GraphQL." Impact: that latency converts directly into booking conversion, and tokens never reaching the browser shrinks the security surface. Invite: "I'd reach for GraphQL Federation only when many per-client BFFs start re-implementing the same composition — SoundCloud migrated exactly that way; before sprawl, a REST BFF is the simpler bet."

10Check yourself — scenario quiz

0 / 9 correct

1. the hotel detail page makes 5 sequential calls to 5 microservices from the browser; it's slow on SEA mobile. What does a BFF primarily fix here?

2. What's the key distinction between a BFF and a general API Gateway?

3. Why is "owned by the frontend team" considered the defining organizational property of a BFF?

4. A reviewer says "putting the auth token in the BFF instead of the browser is just moving the problem." Why is it actually a security win?

Tie this back to the L18 storage triangle.

5. You're already building the web app in Next.js with Server Components and Route Handlers. A teammate proposes adding a separate BFF tier. What's the sharpest response?

6. A new hotel-page BFF has grown to contain pricing rules, loyalty-tier logic, and tax calculation. Why is this a smell?

7. Your org now runs web, iOS, Android, and partner-widget BFFs that all re-implement the same hotel composition, and the count is climbing. What's the recognised next step?

8. An interviewer asks: "Why would you use a BFF instead of GraphQL?" What's the most accurate reframe of this question?

You suggested a BFF; they pushed back with GraphQL. This is the exact interview trap from §5.

9. Your REST BFF for the hotel page has grown to 30+ endpoints. Different page components need different subsets of the same hotel object — card needs name+price, detail needs name+price+reviews+amenities — and you keep adding endpoints. What's the specific trigger that justifies switching to a GraphQL BFF?

Out-loud drill — do this before your interview

In 90 seconds: "Design the data layer for the hotel detail page across web and native." Cover: the waterfall problem, why a BFF per surface, the five jobs (aggregate / shape / orchestrate / secure / degrade), how it maps onto your SSR/RSC server, and — critically — when you'd use a REST vs GraphQL contract on that BFF, and what specific pain triggers each upgrade (GraphQL BFF → Federation).

Good follow-up topics:

Who should own the BFF — FE team or a platform team? How do I cache at the BFF layer without serving stale prices? REST BFF vs GraphQL BFF — show me a real schema comparison Walk me through the token-handler pattern end to end How does the BFF handle partial failure and timeouts? Is an RSC really a BFF, or is that hand-waving? GraphQL Federation deep dive — supergraph, subgraphs, gateway How do I cache GraphQL BFF responses at the CDN layer? How do I test and version a BFF contract?