Client-Adapting APIs
When the server shapes its response to the client (mobile vs web vs partner). The BFF pattern in API form.
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 shapequery MobileHome { user(id: "42") { name avatarThumb unreadCount } feed(limit: 20) { items { id title thumb } }}
# Web asks for a richer shape against the same schemaquery 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.1Host: api.example.comX-Client-Platform: iosX-Client-Version: 7.4.2Accept: application/jsonThe 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.1Accept: application/vnd.example.summary+json ← mobile, trimmed
GET /users/42 HTTP/1.1Accept: application/vnd.example.full+json ← web, full
GET /users/42 HTTP/1.1Accept: application/vnd.example.partner.v3+json ← partner, frozen contractThis 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.
| Decision | Universal API | BFF | GraphQL | Edge adaptation |
|---|---|---|---|---|
| Maintenance | Cheapest | One per client | One schema | One upstream + thin edge |
| Mobile fit | Over-fetches | Tailored | Tailored | Tailored |
| Partner stability | Mixed with internal | Separate BFF | Same schema, different queries | Possible but awkward |
| Caching | URL-level (easy) | URL-level | Per-query (harder) | URL + variant |
| Tooling | REST clients | REST clients | GraphQL clients | REST clients |
| Owns the contract | Platform | Client team | Schema team | Platform |
| Pays off when | One client dominates | 2+ clients diverge | Heterogeneous + changing | Light 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-PlatformorAccept, 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.”
Related concepts#
- GraphQL — A Query Language for APIs — the “BFF in a box” pattern; one endpoint, client-shaped responses.
- Data Fetching Patterns — eager vs lazy, batch vs single; client-adapting design is the policy, fetching pattern is the mechanism.
- Server-Side Rendering vs Client-Side Rendering — the rendering choice shapes the API contract; SSR and CSR pull different things from a BFF.
- REST vs GraphQL vs gRPC — Comparison — REST, GraphQL, gRPC; the architectural-style choice is one input to whether you need a BFF.
- API Versioning — once you have multiple contracts, you have multiple versions; the lifecycle of each matters.