Backend For Frontend

Production engineering guide for backend for frontend covering patterns, implementation strategies, and operational best practices.

A Backend For Frontend (BFF) is a lightweight, API-focused service that sits between a frontend application and the collection of backend services it depends on. It exists when the frontend needs a tailored, cohesive, and performant interface to a set of domain services—especially when those services are too granular, too varied, or too frequently changing to be consumed directly. BFFs matter when a frontend team owns the user experience, but the backend services are independently developed, deployed, and versioned. They are essential when a single page requires data from three or more microservices, when response latency from multiple round trips is unacceptable, or when the frontend needs to transform, aggregate, or enrich data from disparate sources.

Core Responsibilities of a BFF

A BFF performs three primary functions: data aggregation, transformation, and orchestration. It consumes multiple backend APIs—typically HTTP-based, often REST or GraphQL—and produces a single, unified API tailored to the frontend’s needs.

The BFF receives a request from the frontend (e.g., a GET /api/user-profile?include=orders,preferences), then:

  1. Calls multiple backend services in parallel
  2. Merges and transforms the responses
  3. Returns a single, optimized JSON payload

This reduces network overhead, simplifies the frontend’s data layer, and allows the BFF to evolve independently of the underlying services.

Aggregating Data from Multiple Services

The BFF typically uses HTTP clients to call multiple backend services. For example, to serve a user profile, it might call:

Each of these services may have different schemas, response times, and error formats.

A typical BFF endpoint in Node.js using axios might look like:

// src/routes/userProfile.js
const express = require('express');
const axios = require('axios');
const router = express.Router();

router.get('/user-profile', async (req, res) => {
  const userId = req.query.userId;
  const include = req.query.include?.split(',');

  try {
    const [user, orders, preferences, notifications] = await Promise.all([
      axios.get(`https://users-service/api/users/${userId}`),
      axios.get(`https://orders-service/api/orders?userId=${userId}`),
      axios.get(`https://preferences-service/api/preferences?userId=${userId}`),
      axios.get(`https://notifications-service/api/notifications?userId=${userId}`)
    ]);

    const response = {
      user: user.data,
      orders: orders.data,
      preferences: preferences.data,
      notifications: notifications.data,
      timestamp: new Date().toISOString()
    };

    if (include.includes('stats')) {
      response.stats = await calculateUserStats(userId);
    }

    res.json(response);
  } catch (error) {
    res.status(500).json({
      error: 'Failed to fetch user profile',
      details: error.message,
      timestamp: new Date().toISOString()
    });
  }
});

module.exports = router;

This BFF endpoint returns a structured JSON object that the frontend consumes directly, without needing to manage multiple API calls or handle inconsistent response shapes.

Handling Failure and Partial Success

The BFF must handle partial failures gracefully. If one backend service is down, the BFF should not return a 500 error to the frontend, but instead return a partial result with metadata about which parts failed.

For example, if the notifications-service is unreachable:

{
  "user": { "id": 123, "name": "Alice", "email": "[email protected]" },
  "orders": [
    { "id": 101, "total": 245.99, "status": "shipped" }
  ],
  "preferences": { "theme": "dark", "language": "en" },
  "notifications": null,
  "error": {
    "service": "notifications-service",
    "status": 503,
    "message": "Service unavailable",
    "timestamp": "2025-04-05T12:34:56.789Z"
  },
  "timestamp": "2025-04-05T12:34:56.789Z"
}

The frontend can display the user profile with the available data, while showing a “Loading notifications…” indicator or a toast for the failure.

Data Transformation and Enrichment

The BFF transforms data from disparate sources into a single, consistent shape. This includes:

Example of enriching data using a mapping function:

function enrichUserProfile(user, orders, preferences) {
  const enriched = { ...user };

  enriched.orders = orders.map(order => ({
    ...order,
    total: order.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    statusLabel: mapStatusToLabel(order.status)
  }));

  enriched.preferences = {
    theme: preferences.theme || 'light',
    language: preferences.language || 'en',
    notificationsEnabled: preferences.notifications ?? true
  };

  enriched.role = ROLE_MAP[enriched.groupId] || 'user';

  return enriched;
}

This function can run in the BFF before sending the final response, ensuring consistency across the frontend.

BFF Configuration and Deployment

A BFF is typically deployed as a lightweight service—often a Docker container—running on Kubernetes, AWS EC2, or a serverless platform like AWS Lambda.

A typical Dockerfile:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

A kubernetes.yml snippet for deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-profile-bff
spec:
  replicas: 4
  selector:
    matchLabels:
      app: user-profile-bff
  template:
    metadata:
      labels:
        app: user-profile-bff
    spec:
      containers:
        - name: bff
          image: registry.example.com/bff/user-profile:latest
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: bff-secrets
                  key: db-url
            - name: NOTIFICATIONS_SERVICE_URL
              value: "https://notifications-service/api"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "200m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 5

The BFF exposes health and readiness endpoints (GET /health, GET /ready) that are used by the orchestrator to manage rolling updates and ensure zero-downtime deployments.

BFF as a Layer in the Architecture

The BFF sits at the edge of the system, acting as a coordinator between frontend and backend services. It decouples the frontend from backend changes, allowing:

A typical BFF request flow:

  1. Frontend sends GET /api/v2/user-profile?userId=123&include=orders,stats
  2. BFF checks cache (Redis) for key user-profile:123:v2
  3. If cache miss, BFF calls four backend services in parallel
  4. BFF merges and enriches data, adds stats if requested
  5. BFF stores result in Redis with TTL of 5 minutes
  6. BFF returns JSON to frontend, including Cache-Control: public, max-age=300

Common Pitfalls and Sharp Edges

1. Stale Data and Cache Invalidation

The BFF may serve stale data when backend services update asynchronously. A user updates their preferences, but the BFF’s cached profile still shows the old values.

A common mistake: assuming that cache invalidation happens automatically. The BFF must explicitly invalidate cache on updates.

Example: when a user updates their profile, the backend service emits an event:

{
  "event": "user.updated",
  "userId": 123,
  "timestamp": "2025-04-05T12:35:00Z"
}

The BFF listens to this event and invalidates the cache key user-profile:123:v2.

Failure mode: cache invalidation fails silently—e.g., Redis is down, but no error is logged. The BFF continues serving stale data for hours.

2. Error Propagation and Logging

When the BFF calls multiple services, it must propagate errors clearly. The frontend should be able to distinguish between:

The BFF should log detailed error traces, including:

Example log entry:

{
  "requestId": "a1b2c3d4e5",
  "timestamp": "2025-04-05T12:36:00.123Z",
  "service": "user-profile-bff",
  "level": "error",
  "message": "Failed to fetch user profile",
  "details": {
    "userId": 123,
    "include": ["orders", "stats"],
    "calls": [
      {
        "service": "users-service",
        "url": "https://users-service/api/users/123",
        "status": 200,
        "durationMs": 12,
        "response": { "id": 123, "name": "Alice" }
      },
      {
        "service": "orders-service",
        "url": "https://orders-service/api/orders?userId=123",
        "status": 504,
        "durationMs": 1034,
        "error": "Gateway Timeout",
        "request": { "userId": 123 },
        "response": null
      },
      {
        "service": "preferences-service",
        "url": "https://preferences-service/api/preferences?userId=123",
        "status": 200,
        "durationMs": 42,
        "response": { "theme": "dark" }
      }
    ]
  }
}

3. Over-aggregation and Over-fetching

A BFF can become too complex—calling every possible backend service even when only a subset is needed. The result is a slow, over-optimized endpoint that is hard to maintain.

Example: the BFF calls notifications-service, user-service, orders-service, products-service, analytics-service, and settings-service for every profile request—even when only orders and preferences are needed.

The solution: use query parameters to control which data to fetch.

GET /api/user-profile?userId=123&include=orders,preferences,stats&cache=true

The BFF parses include and fetches only the requested data. It can also return a Link header to guide the frontend:

Link: <https://bff.example.com/api/user-profile?userId=123&include=notifications>; rel="notifications"

4. BFF as a Service Mesh Node

The BFF can be part of a service mesh (e.g., Istio, Linkerd) and thus benefit from:

A BFF can be the entry point to a domain or application, handling all cross-cutting concerns before delegating to backend services.

When to Use a BFF

Use a BFF when:

Do not use a BFF when:

A BFF is not a one-size-fits-all solution. It is a deliberate choice to reduce complexity at the edge, where the user experience is most sensitive.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.