Backend for Frontend (BFF): Building Client-Optimised APIs That Scale

Backend for Frontend (BFF): Building Client-Optimised APIs That Scale
Table of Contents
- The General Purpose API Problem
- How the BFF Pattern Works
- The Aggregation Advantage: N+1 → 1 Server-Side
- BFF Ownership Model: Frontend Teams Own Their Backend
- BFF vs API Gateway: Complementary, Not Competing
- Implementation: Node.js BFF for Mobile
- GraphQL as a BFF: The Self-Describing Alternative
- Authentication in the BFF Layer
- Performance Patterns: Parallel Fetching and Caching
- When BFF is Overkill
- Frequently Asked Questions
- Key Takeaway
The General Purpose API Problem
The traditional approach creates a single API that all clients must work with:
Client: Mobile App (limited bandwidth, battery)
Calls: GET /users/123
Response:
{
"id": 123,
"firstName": "Alice",
"lastName": "Smith",
"email": "alice@example.com", ↠Mobile only shows initials
"phoneNumber": "+44-xxx-xxxxx", ↠Mobile doesn't use this
"address": { entire object }, ↠Not shown in mobile list view
"preferences": { 50 fields }, ↠Mobile ignores all
"auditHistory": [ 200 entries ], ↠Mobile never shows this
"billingDetails": { ... }, ↠Mobile doesn't handle payments
"permissions": [...] ↠Mobile checks different permissions
}
Mobile only uses: id, firstName, lastName (3 fields out of 80+)
= 96% data over-fetched every single API callWhen the desktop needs more data than mobile provides:
Client: React Dashboard (high bandwidth, complex UI)
Needs: User + their Orders + Order totals + Recent Activity
Calls needed:
GET /users/123 → 1st network request
GET /orders?userId=123 → 2nd network request (after 1st completes)
GET /activity?userId=123 → 3rd network request
→ 3 sequential round trips on a slow cellular connection = 900msHow the BFF Pattern Works
Each BFF is a lightweight server-side aggregation layer — it does not contain business logic (that lives in the microservices), only:
- Request aggregation (fan out to multiple services, merge responses)
- Response shaping (strip unnecessary fields, restructure for the client)
- Authentication token transformation (exchange tokens between client and microservice formats)
- Client-specific caching
The Aggregation Advantage: N+1 → 1 Server-Side
The biggest performance gain: moving network fan-out from client to server.
BEFORE BFF (client-side aggregation over 5G):
Mobile App → GET /users/123 → User Service (200ms over 5G)
Mobile App → GET /orders?userId=123 → Orders (200ms over 5G, must wait for step 1)
Total: 400ms minimum (sequential), all over mobile network
AFTER BFF (server-side aggregation over gigabit LAN):
Mobile App → GET /dashboard → Mobile BFF (200ms over 5G for 1 round trip)
Mobile BFF → GET /users/123 (parallel) (5ms internal LAN)
Mobile BFF → GET /orders?userId=123 (parallel) (5ms internal LAN, concurrent with users)
Mobile BFF merges + shapes response (1ms)
Total: 200ms + 5ms = 205ms — 2× faster, 1 network round tripBFF Ownership Model: Frontend Teams Own Their Backend
The critical organisational insight of BFF: the team that owns the frontend owns the BFF.
Traditional model:
Mobile Team: "We need the user + recent orders merged in one call"
Backend Team: "We'll add it to the roadmap" (3 sprints later...)BFF model:
Mobile Team: "We need user + orders merged"
Mobile Team: builds /mobile/dashboard endpoint in Mobile BFF (today)This removes the cross-team dependency bottleneck — frontend developers can evolve their API contract at the speed of UI changes, not at the speed of backend sprint planning.
BFF vs API Gateway: Complementary, Not Competing
These two patterns are frequently confused:
| Concern | API Gateway | BFF |
|---|---|---|
| Primary purpose | Security, routing, rate limiting | Client-specific aggregation and shaping |
| Owned by | Platform/infrastructure team | Frontend team |
| Knowledge of client | None — generic | Deep — shaped for specific client |
| Business logic | None | None (only aggregation/shaping) |
| Number of instances | One (or one per environment) | One per client type |
| Authentication | Validates token at entry | May transform tokens for downstream services |
Best practice: Both together.
Mobile App → Mobile BFF → API Gateway → Microservices
API Gateway handles: TLS termination, rate limiting, JWT validation, routing
Mobile BFF handles: Aggregation, field filtering, mobile-specific auth flowsImplementation: Node.js BFF for Mobile
// mobile-bff/src/routes/dashboard.ts
import { Router, Request, Response } from 'express';
const router = Router();
// Mobile dashboard: single endpoint aggregates 3 microservice calls
router.get('/dashboard', async (req: Request, res: Response) => {
const userId = req.user.id; // from JWT middleware
try {
// Fan out: call multiple services in PARALLEL (not sequential)
const [user, orders, activity] = await Promise.all([
userServiceClient.getUser(userId),
orderServiceClient.getRecentOrders(userId, { limit: 5 }),
activityServiceClient.getRecentActivity(userId, { limit: 10 }),
]);
// Shape response for mobile: only the fields mobile UI needs
const mobileResponse = {
// From user service (80 fields → 4 fields for mobile):
user: {
id: user.id,
displayName: `${user.firstName} ${user.lastName[0]}.`,
avatarUrl: user.profile.avatarUrl,
tier: user.subscriptionTier,
},
// From order service (20 fields per order → 4 fields):
recentOrders: orders.items.map(order => ({
id: order.id,
summary: order.items.map(i => i.productName).join(', '),
total: formatCurrency(order.totalAmount, order.currency),
status: order.status,
})),
// From activity service:
unreadNotifications: activity.filter(a => !a.read).length,
};
res.json(mobileResponse);
} catch (error) {
// BFF handles partial failures gracefully:
if (error.service === 'activity') {
// Non-critical — return dashboard without activity data:
const [user, orders] = await Promise.all([...]);
res.json({ user: shapeUser(user), recentOrders: shapeOrders(orders), unreadNotifications: 0 });
} else {
res.status(503).json({ error: 'Service temporarily unavailable' });
}
}
});GraphQL as a BFF: The Self-Describing Alternative
GraphQL intrinsically solves the over/under-fetching problem — clients declare exactly what data they need. A GraphQL BFF gives clients even more control:
# Web client queries full user profile:
query {
user(id: "123") {
id
firstName
lastName
email
address { street, city, postcode }
subscriptionTier
billingDetails { nextBillingDate, amount }
recentOrders(limit: 20) { id, status, total, createdAt }
}
}
# Mobile client queries minimal profile (same API, less data):
query {
user(id: "123") {
id
displayName # computed field — concatenated by resolver
avatarUrl
subscriptionTier
recentOrders(limit: 5) { id, status, total }
}
}GraphQL as a BFF works particularly well when you have many client types with genuinely different data needs and you want a single BFF to serve them all via schema-level flexibility.
Frequently Asked Questions
How many BFFs should I have? One per meaningfully distinct client type — typically: Web BFF, Mobile BFF (or separate iOS/Android if they diverge significantly), and optionally a Third-Party API BFF (a public-facing, versioned, stable API for external partners, which has different versioning requirements than the Web or Mobile BFFs). Resist the temptation to create a BFF per feature team — that creates a maze of entry points.
Does the BFF violate the DRY principle since Web and Mobile BFFs may share similar aggregation code? Use a shared library for common patterns (authentication token handling, service client configuration, error formatting), but explicitly accept duplication in the aggregation endpoints. The BFF endpoints diverge quickly as client needs evolve — shared aggregation code between Web and Mobile BFFs becomes a coupling point that slows both teams. The cost of some duplication is lower than the cost of tight coupling between teams.
Key Takeaway
The Backend for Frontend pattern is fundamentally about team topology and API ownership. It gives frontend teams the autonomy to evolve their API contract at the speed of UI development, without negotiating with backend teams for every new data requirement. The technical benefits — reduced over-fetching, server-side aggregation, client-specific resilience — are real and measurable. Start with a BFF when frontend teams are blocked waiting for backend API changes, or when your mobile app's network performance is suffering from too many sequential requests.
Read next: Circuit Breaker Pattern: Stopping Cascading Failures →
Part of the Software Architecture Hub — comprehensive guides from architectural foundations to advanced distributed systems patterns.
