Client-Adapting APIs

When the server shapes its response to the client (mobile vs web vs partner). The BFF pattern in API form.

Concept Intermediate
10 min read
bff mobile web graphql api-design

Summary#

A client-adapting API shapes its response to the consumer asking. The mobile app gets a trimmed payload optimised for cellular networks. The web app gets a richer payload with full descriptions and nested relations. A partner integration gets a stable, slowly-evolving contract with no UI-specific fields. One backend, three contracts.

The pattern most commonly takes the form of a Backend-for-Frontend (BFF) — one server (or one layer) per consumer, sitting between the consumer and a pool of underlying microservices. The mobile BFF aggregates from the same downstream services as the web BFF, but composes and trims differently. SoundCloud coined the term around 2015; Netflix’s “device-specific edge services” pre-dated it. GraphQL is the modern “BFF in a box” — one endpoint, the client declares the shape it needs.

The alternative is a universal API — one contract, every consumer takes what they get. Universal APIs are simpler to maintain (one codebase, one schema, one set of tests). They are also chronically over-fetched on mobile and chronically under-detailed on web.

The right call depends on whether your consumers differ enough to make the maintenance cost of multiple contracts pay back. Two clients with the same shape? Ship a universal API. Three clients with three radically different display surfaces and three radically different network budgets? Ship a BFF per consumer or adopt GraphQL.

Why it matters#

Three reasons client-adapting design comes up in nearly every consumer-facing API review:

  • Mobile and web have different physics. A mobile client on a flaky cellular network with a 4-inch screen does not want the same payload as a web client on a fibre connection with a 27-inch monitor. Over-fetching on mobile wastes the user’s battery and data; under-fetching on web forces extra round-trips. One contract picks a side; client-adapting design lets both win.
  • Public APIs and internal APIs evolve at different speeds. A partner who integrated three years ago is not going to redeploy because you renamed a field. Your own mobile team will. A client-adapting layer lets the public surface freeze while the internal one moves.
  • The alternative — bloated universal contracts — rots. Every UI team adds the field they need; nobody removes anything (a partner might depend on it). Five years in, the response is 40 KB of JSON of which any one client uses 3 KB. The cost is paid every request, forever.

The senior signal in an interview: “We can ship one universal API today, but if mobile and web have different display surfaces and different network budgets, we should plan for a BFF layer — or GraphQL — within the first year.”

How it works#

The BFF pattern#

A BFF is a thin server that sits between a specific client and the underlying microservices. It does three things: aggregate (one request to the BFF fans out to N downstream calls), trim (return only the fields the client renders), and adapt (reshape names, units, enumerations to what the client wants).

Mobile app Mobile BFF Microservices
│ GET /home │ │
│─────────────────────►│ │
│ │ GET /user/42 │
│ │─────────────────────────►│
│ │ GET /feed/42?limit=20 │
│ │─────────────────────────►│
│ │ GET /notifications/42 │
│ │─────────────────────────►│
│ │ trim + compose │
│ 200 OK │ │
│ { trimmed home } │ │
│◄─────────────────────│ │
Web app Web BFF Microservices
│ GET /home │ │
│─────────────────────►│ │
│ │ GET /user/42 │
│ │─────────────────────────►│
│ │ GET /feed/42?limit=50 │ (more items)
│ │─────────────────────────►│
│ │ GET /notifications/42 │
│ │─────────────────────────►│
│ │ GET /recommendations/42 │ (extra service)
│ │─────────────────────────►│
│ │ compose full │
│ 200 OK │ │
│ { full home } │ │
│◄─────────────────────│ │

The mobile BFF requests 20 feed items; the web BFF requests 50 and also calls the recommendations service. Both BFFs use the same underlying microservices — the divergence is at the edge, not at the data layer.

Practically, a BFF is owned by the client team that consumes it. The mobile team writes the mobile BFF; the web team writes the web BFF. This is the part teams routinely get wrong: if a platform team owns the BFF, it becomes a universal API with extra hops. The whole point is that the BFF moves at the client’s release cadence.

GraphQL as the BFF in a box#

A GraphQL endpoint inverts the BFF idea: instead of the server picking what to return per client, the client declares the shape it wants. One endpoint, one schema, every consumer extracts a sub-graph.

# Mobile asks for a trimmed shape
query MobileHome {
user(id: "42") {
name
avatarThumb
unreadCount
}
feed(limit: 20) {
items { id title thumb }
}
}
# Web asks for a richer shape against the same schema
query WebHome {
user(id: "42") {
name
fullName
avatarLarge
unreadCount
recommendations { id title image score }
}
feed(limit: 50) {
items { id title description thumb publishedAt author { name } }
}
}

The server stitches the same downstream services either way; the client controls the shape. Facebook moved their mobile clients to GraphQL in 2012 for exactly this reason — they were tired of building a new endpoint every time the mobile design changed. Shopify, GitHub, and Netflix shipped public GraphQL APIs on the same logic.

GraphQL costs are real (the N+1 fan-out problem, query-complexity attacks, caching becomes per-query not per-URL). It is the right call when client requirements are heterogeneous and changing. It is the wrong call when one client owns 95% of the traffic and the schema is stable.

Edge-side adaptation — Cloudflare Workers, Vercel Edge#

A lighter version of the BFF idea: a JavaScript function at the CDN edge reshapes a single upstream response per client. The upstream API stays universal; the edge layer trims and adapts.

Mobile client Edge function Origin API
│ GET /v1/home │ │
│───────────────►│ │
│ │ GET /v1/home │
│ │───────────────────►│
│ │ 200 (full) │
│ │◄───────────────────│
│ │ detect mobile UA │
│ │ trim fields │
│ 200 (trim) │ │
│◄───────────────│ │

This is the Cloudflare Workers / Vercel Edge play: one upstream contract, several edge personalities. Faster to ship than a real BFF, less powerful (no fan-out, no cross-service composition).

Adapting by header, not by URL#

Whichever pattern you pick, do the routing on a request header (User-Agent, a custom X-Client-Platform, Accept content-negotiation), not by version-in-the-URL. URL-versioned adapters (/mobile/home, /web/home) leak the consumer identity into the path; if a new client appears or one is renamed, every link breaks.

GET /v1/home HTTP/1.1
Host: api.example.com
X-Client-Platform: ios
X-Client-Version: 7.4.2
Accept: application/json

The server reads X-Client-Platform and routes to the mobile BFF (or the mobile branch in a universal handler). The URL stays stable; the contract specialises by header.

Adapting by Accept content type#

The most HTTP-native form of client adaptation is to vary the response by Accept:

GET /users/42 HTTP/1.1
Accept: application/vnd.example.summary+json ← mobile, trimmed
GET /users/42 HTTP/1.1
Accept: application/vnd.example.full+json ← web, full
GET /users/42 HTTP/1.1
Accept: application/vnd.example.partner.v3+json ← partner, frozen contract

This is the right RESTful answer. It composes cleanly with HTTP caches (Vary: Accept), it leaves the URL stable, and it makes the client identity explicit in the request. The cost: it’s verbose and most client libraries don’t make it easy.

Variants and trade-offs#

One universal API. Single contract, every client takes what’s there. Simplest to maintain — one codebase, one schema, one set of tests. Right call when consumers have similar shapes and bandwidth budgets, or when you have one client. Wrong call when mobile and web diverge significantly.

BFF per consumer. One backend per client (mobile, web, partner). Owned by the client team, moves at the client’s release cadence. Earns its weight when consumers differ enough that the maintenance cost beats the over-fetch cost. Most powerful, most expensive to staff.

DecisionUniversal APIBFFGraphQLEdge adaptation
MaintenanceCheapestOne per clientOne schemaOne upstream + thin edge
Mobile fitOver-fetchesTailoredTailoredTailored
Partner stabilityMixed with internalSeparate BFFSame schema, different queriesPossible but awkward
CachingURL-level (easy)URL-levelPer-query (harder)URL + variant
ToolingREST clientsREST clientsGraphQL clientsREST clients
Owns the contractPlatformClient teamSchema teamPlatform
Pays off whenOne client dominates2+ clients divergeHeterogeneous + changingLight differences

A pragmatic progression for a growing product: start universal, add a mobile BFF when mobile over-fetch becomes measurable, add a partner BFF when the public API needs to freeze independently, consider GraphQL when you have three or more clients all asking for different shapes. Don’t pre-build BFFs you don’t need — each one is a deployable to operate.

When this is asked in interviews#

Client-adapting design comes up in three places:

  • Anywhere the interviewer mentions “we have a mobile app and a web app” as part of the requirements. The senior answer asks: do they need different shapes? Different volumes? Different SLAs? If yes, propose a BFF layer (or GraphQL); if no, one universal API is fine.
  • In any partner-API design. Partners ship slower than your own product. The senior answer separates the partner contract from the internal one — a partner BFF, a separate API version, or a different endpoint family.
  • In any mobile-first design. The follow-up: how do you keep the payload small? Sparse fieldsets, BFF trimming, GraphQL — name one and defend it.

Specific points to make:

  • Name the BFF pattern explicitly. Reference SoundCloud / Netflix as origin; reference Facebook’s GraphQL adoption as the “BFF in a box” case.
  • Defend the boundary. Who owns the BFF? The client team, not the platform team. Otherwise it becomes a universal API with extra hops.
  • Route by header, not by URL. X-Client-Platform or Accept, not /mobile/home. Keeps the URL space stable.
  • Acknowledge the cost. N BFFs is N services to operate; GraphQL is its own complexity (N+1, query depth, caching). Universal APIs are simplest — start there.
  • Tie it to evolution. A BFF or GraphQL layer is also the place to absorb a breaking change in the underlying service without breaking the client.

The strongest one-liner: “If the consumers differ on shape, volume, or release cadence, we put a BFF or a GraphQL layer between them and the microservices. The universal API is the default; we move to BFF when the cost of over-fetch and contract collision exceeds the cost of operating N edges.”

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.