Software ArchitectureSystem Design

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

TT
TopicTrick Team
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

The traditional approach creates a single API that all clients must work with:

text
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 call

When the desktop needs more data than mobile provides:

text
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 = 900ms

How 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.

text
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 trip

BFF Ownership Model: Frontend Teams Own Their Backend

The critical organisational insight of BFF: the team that owns the frontend owns the BFF.

Traditional model:

text
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:

text
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:

ConcernAPI GatewayBFF
Primary purposeSecurity, routing, rate limitingClient-specific aggregation and shaping
Owned byPlatform/infrastructure teamFrontend team
Knowledge of clientNone — genericDeep — shaped for specific client
Business logicNoneNone (only aggregation/shaping)
Number of instancesOne (or one per environment)One per client type
AuthenticationValidates token at entryMay transform tokens for downstream services

Best practice: Both together.

text
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 flows

Implementation: Node.js BFF for Mobile

typescript
// 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:

graphql
# 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.