Backend for Frontend (BFF): Tailoring APIs for UI
The One-Size-Fits-All API Problem
A "one-size-fits-all" API causes bloated mobile payloads and underpowered desktop dashboards.
π― System Scenario & Requirements
System Goal
A "one-size-fits-all" API causes bloated mobile payloads and underpowered desktop dashboards.
Actors & Components
- Client Layer
- BFF Layer
- Core Services
- Data & Persistence
- Mobile Client
- Web Client
- Mobile BFF
- Web BFF
Functional Requirements
- TLDR: A "one-size-fits-all" API causes bloated mobile payloads and underpowered desktop dashboards.
- The Backend for Frontend (BFF) pattern solves this by creating a dedicated API server for each client type β the mobile BFF reshapes data for small screens, the web BFF aggregates for dashboards.
- You have a mobile app and a web dashboard.
Constraints & Assumptions
- BFF is not always the right answer: Code duplication risk: If mobile and web BFFs share 80% of logic, refactor the common parts into shared service clients β not into both BFFs.
β‘ Execution Stages & Decision Trace
Stage 1 β BFF Basics: One Backend Per Frontend
A generic REST API returns the same response shape to every caller.
That works for simple apps but breaks down when clients have radically different needs: - Mobile apps run on limited bandwidth and battery.
Deep Dive
Scenario context
A "one-size-fits-all" API causes bloated mobile payloads and underpowered desktop dashboards. The Backend for Frontend (BFF) pattern solves this by creating a dedicated API server for each client type β the mobile BFF reshapes data for small screens, the web BFF aggregates for dashboards.
Observable outcome: - Protocol bridging: Microservices may speak gRPC internally.
Stage 3 β Request Flow Through a BFF
- The request lifecycle from client to microservices flows through the BFF: Each step adds value: 1.
- Auth validation β the BFF verifies the token before hitting any upstream service.
- Parallel upstream calls β fan out to N services simultaneously to minimize latency.
- Response aggregation β merge the N responses into one JSON object.
flowchart TD
A[Client Request] --> B{Which client?}
B -->|Mobile| C[Mobile BFF]
B -->|Web| D[Web BFF]
C --> E[Auth Token Validation]
D --> E
E --> F[Parallel Upstream Calls]
F --> G[User Service]
F --> H[Cart Service]
F --> I[Rec Service]
G --> J[Response Aggregation]
H --> J
I --> J
J --> K[Field Projection]
K --> L[Client Response]
sequenceDiagram
participant MB as Mobile App
participant MBff as Mobile BFF
participant WB as Web Dashboard
participant WBff as Web BFF
participant US as User Service
participant CS as Cart Service
participant RS as Rec Service
MB->>MBff: GET /mobile/home/{userId}
par parallel fetch
MBff->>US: getUser(userId)
US-->>MBff: firstName only
MBff->>CS: getCart(userId)
CS-->>MBff: itemCount only
end
MBff-->>MB: compact 8-field response
WB->>WBff: GET /web/dashboard/{userId}
par parallel fetch
WBff->>US: getUser(userId)
US-->>WBff: full profile
WBff->>CS: getCart(userId)
CS-->>WBff: full cart detail
WBff->>RS: getTopRecs(userId)
RS-->>WBff: 10 recommendations
end
WBff-->>WB: aggregated dashboard payload
Observable outcome: Response aggregation β merge the N responses into one JSON object.
Stage 4 β Deep Dive: What Lives Inside a BFF
- A BFF is not a full microservice.
- It is a lightweight orchestration layer.
- Typical responsibilities: A BFF should contain no business logic .
- If you find yourself writing pricing rules or fraud detection inside a BFF, that code belongs in a separate service.
Observable outcome: If you find yourself writing pricing rules or fraud detection inside a BFF, that code belongs in a separate service.
Stage 5 β Deep Dive: Parallel Aggregation: The Common BFF Pattern
- The most useful thing a BFF does is fan out requests and merge them: Three upstream calls run in parallel (~80 ms total instead of sequential ~240 ms).
- The client gets a single clean response in one round trip.
# Mobile BFF: home screen endpoint
async def get_home_screen(user_id: str):
user, cart, recommendations = await asyncio.gather(
user_service.get(user_id),
cart_service.get(user_id),
rec_service.get_top5(user_id),
)
return {
"name": user.first_name,
"cart_count": len(cart.items),
"recs": [r.title for r in recommendations],
}
Observable outcome: The client gets a single clean response in one round trip.
Stage 6 β Practical: Adding a New Mobile Screen
- Suppose you add a "notifications center" screen to the mobile app.
- The workflow with a BFF: Step 1 β Identify what the screen needs: - Unread notification count (from Notification Service) - Top 3 notification previews (from Notification Service) - User's preferred notification type (fβ¦
- The web dashboard has its own endpoint for notifications with a different shape.
- This is the BFF promise: frontend-driven API changes stay contained to that frontend's BFF .
# Mobile BFF: notifications screen endpoint
async def get_notifications_screen(user_id: str):
notifs, prefs = await asyncio.gather(
notification_service.get_recent(user_id, limit=3),
user_service.get_notification_prefs(user_id),
)
return {
"unread_count": notifs.total_unread,
"previews": [{"id": n.id, "text": n.text[:50]} for n in notifs.items],
"sound_on": prefs.sound_enabled,
}
Observable outcome: This is the BFF promise: frontend-driven API changes stay contained to that frontend's BFF .
End-to-end outcome
You have a mobile app and a web dashboard.
Alternatives considered
The source focuses on one primary design. A second architecture is not compared, so no alternative is invented here.
Failure and degraded modes
Trade-offs & Failure Modes: Trade-offs, Failure Modes & Decision Guide
- Detection: BFF is not always the right answer: Code duplication risk: If mobile and web BFFs share 80% of logic, refactor the common parts into shared service clients β not into both BFFs.
- User or operator impact: BFF is not always the right answer: Code duplication risk: If mobile and web BFFs share 80% of logic, refactor the common parts into shared service clients β not into both BFFs.
