Skip to content
All essays
ArchitectureMarch 27, 202414 min

Backend for Frontend (BFF) Pattern

Optimize client-server communication with BFF pattern for better user experience.

Ü
Ümit Uz
Mobile & Full Stack Developer

The Backend for Frontend (BFF) pattern creates dedicated backend services for different client applications, optimizing data delivery for each platform's unique needs.

What Is BFF?

BFF sits between the client and backend services, shaping data specifically for each frontend platform.

text
┌──────────┐     ┌──────────┐
│ Web App  │────▶│ Web BFF  │
└──────────┘     └────┬─────┘
                      │
                      ▼
              ┌───────────────┐
              │ Backend APIs  │
              └───────┬───────┘
                      │
              ┌───────┴───────┐
              │               │
         ┌────┴────┐     ┌────┴────┐
         │Mobile BFF│     │Desktop BFF│
         └─────────┘     └──────────┘
              ↑                  ↑
         ┌───┴────┐         ┌────┴───┐
         │Mobile  │         │Desktop │
         │App     │         │App     │
         └────────┘         └────────┘

Why Use BFF?

Platform-Specific Needs

Different clients need different data shapes.

javascript
// Web needs detailed data
{
  "product": {
    "id": "123",
    "name": "Product Name",
    "description": "Detailed description...",
    "images": ["url1", "url2", "url3"],
    "specs": { /* detailed specs */ },
    "reviews": [ /* all reviews */ ]
  }
}

// Mobile needs minimal data
{
  "product": {
    "id": "123",
    "name": "Product Name",
    "thumbnail": "url1",
    "price": 99.99
  }
}

Reduced Over-fetching

javascript
// Without BFF - mobile gets too much data
GET /api/products/123
// Returns 50KB of data mobile doesn't need

// With BFF - mobile gets what it needs
GET /api/mobile/products/123
// Returns 2KB of relevant data

Implementation Patterns

Per-Client BFF

javascript
// Web BFF
app.get('/api/web/products/:id', async (req, res) => {
  const [product, reviews, recommendations, similar] = await Promise.all([
    fetchProduct(req.params.id),
    fetchReviews(req.params.id),
    fetchRecommendations(req.params.id),
    fetchSimilarProducts(req.params.id),
  ]);

  res.json({
    product: {
      ...product,
      fullDescription: product.description,
      allImages: product.images,
      specifications: product.specs,
    },
    reviews: reviews.map(r => ({
      id: r.id,
      user: r.user.name,
      rating: r.rating,
      comment: r.comment,
      date: r.createdAt,
    })),
    recommendations,
    similar,
  });
});

// Mobile BFF
app.get('/api/mobile/products/:id', async (req, res) => {
  const product = await fetchProduct(req.params.id);

  res.json({
    id: product.id,
    name: product.name,
    price: product.price,
    thumbnail: product.images[0],
    rating: product.averageRating,
  });
});

Feature-Based BFF

javascript
// Dashboard BFF
app.get('/api/dashboard', async (req, res) => {
  const [stats, recentOrders, notifications] = await Promise.all([
    fetchUserStats(req.user.id),
    fetchRecentOrders(req.user.id, 5),
    fetchNotifications(req.user.id),
  ]);

  res.json({
    stats,
    recentOrders,
    notifications,
  });
});

// Checkout BFF
app.get('/api/checkout/summary', async (req, res) => {
  const [cart, shipping, payment] = await Promise.all([
    fetchCart(req.user.id),
    fetchShippingOptions(),
    fetchPaymentMethods(req.user.id),
  ]);

  res.json({
    items: cart.items,
    subtotal: cart.total,
    shipping: shipping.options,
    payment: payment.methods,
    total: calculateTotal(cart, shipping),
  });
});

Data Aggregation

Parallel Fetching

javascript
app.get('/api/mobile/home', async (req, res) => {
  const [featured, categories, deals] = await Promise.all([
    fetchFeaturedProducts(),
    fetchCategories(),
    fetchDailyDeals(),
  ]);

  res.json({
    featured: featured.slice(0, 5),
    categories: categories.map(c => ({
      id: c.id,
      name: c.name,
      icon: c.iconUrl,
    })),
    deals: deals.map(d => ({
      id: d.id,
      title: d.title,
      discount: d.discountPercentage,
      price: d.price,
    })),
  });
});

Sequential Data Fetching

javascript
app.get('/api/web/product/:id', async (req, res) => {
  // First, get the product
  const product = await fetchProduct(req.params.id);

  // Then, use product data to fetch related info
  const [reviews, recommendations] = await Promise.all([
    fetchReviews(req.params.id),
    fetchRecommendations(product.categoryId),
  ]);

  res.json({
    product,
    reviews,
    recommendations,
  });
});

Caching Strategies

Client-Specific Caching

javascript
const webCache = new NodeCache({ stdTTL: 3600 });
const mobileCache = new NodeCache({ stdTTL: 7200 });

app.get('/api/web/products/:id', async (req, res) => {
  const cacheKey = `product:${req.params.id}`;
  let product = webCache.get(cacheKey);

  if (!product) {
    product = await fetchProduct(req.params.id);
    webCache.set(cacheKey, product);
  }

  res.json(product);
});

app.get('/api/mobile/products/:id', async (req, res) => {
  const cacheKey = `product:${req.params.id}`;
  let product = mobileCache.get(cacheKey);

  if (!product) {
    product = await fetchProduct(req.params.id);
    mobileCache.set(cacheKey, product);
  }

  // Transform for mobile
  res.json({
    id: product.id,
    name: product.name,
    price: product.price,
  });
});

Error Handling

Graceful Degradation

javascript
app.get('/api/web/dashboard', async (req, res) => {
  try {
    const [stats, orders, notifications] = await Promise.all([
      safeFetch(fetchUserStats, { defaultValue: {} }),
      safeFetch(fetchRecentOrders, { defaultValue: [] }),
      safeFetch(fetchNotifications, { defaultValue: [] }),
    ]);

    res.json({
      stats,
      orders,
      notifications,
    });
  } catch (error) {
    res.status(500).json({
      error: 'Partial failure',
      data: {
        stats: {},
        orders: [],
        notifications: [],
      },
    });
  }
});

async function safeFetch(fn, { defaultValue }) {
  try {
    return await fn();
  } catch (error) {
    console.error('Fetch failed:', error);
    return defaultValue;
  }
}

Versioning

API Versioning

javascript
// Web BFF v1
app.get('/api/v1/web/products/:id', async (req, res) => {
  const product = await fetchProduct(req.params.id);
  res.json({ product });
});

// Web BFF v2 with new features
app.get('/api/v2/web/products/:id', async (req, res) => {
  const [product, availability, shippingInfo] = await Promise.all([
    fetchProduct(req.params.id),
    fetchAvailability(req.params.id),
    fetchShippingInfo(req.params.id),
  ]);

  res.json({
    product,
    availability,
    shippingInfo,
  });
});

Performance Optimization

Response Compression

javascript
const compression = require('compression');

// Compress all responses
app.use(compression());

// Or selective compression
app.get('/api/mobile/*', compression({
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  },
}));

Field Selection

javascript
app.get('/api/web/products/:id', async (req, res) => {
  const { fields } = req.query;

  const product = await fetchProduct(req.params.id);

  if (fields) {
    const selectedFields = fields.split(',');
    const filtered = {};

    selectedFields.forEach(field => {
      if (product[field] !== undefined) {
        filtered[field] = product[field];
      }
    });

    return res.json(filtered);
  }

  res.json(product);
});

// Usage: /api/web/products/123?fields=id,name,price

Testing

Unit Testing

javascript
import request from 'supertest';
import app from './app';

describe('Web BFF', () => {
  it('should return full product details', async () => {
    const response = await request(app)
      .get('/api/web/products/123')
      .expect(200);

    expect(response.body).toHaveProperty('product');
    expect(response.body).toHaveProperty('reviews');
    expect(response.body).toHaveProperty('recommendations');
  });
});

describe('Mobile BFF', () => {
  it('should return minimal product data', async () => {
    const response = await request(app)
      .get('/api/mobile/products/123')
      .expect(200);

    expect(response.body).toHaveProperty('id');
    expect(response.body).toHaveProperty('name');
    expect(response.body).toHaveProperty('price');
    expect(response.body).not.toHaveProperty('detailedSpecs');
  });
});

Best Practices

DOs

  • Keep BFF thin - it's a layer, not a service
  • Optimize data for each client
  • Implement caching strategically
  • Handle failures gracefully
  • Monitor performance per BFF
  • Version your BFF APIs
  • Document client-specific optimizations

DON'Ts

  • Add business logic to BFF
  • Create too many BFFs
  • Over-fetch data
  • Ignore client constraints
  • Skip error handling
  • Make BFF a bottleneck
  • Duplicate complex logic across BFFs

Real-World Example

E-commerce Platform

javascript
// Web BFF for product page
app.get('/api/web/products/:id', async (req, res) => {
  const [product, reviews, qna, recommendations] = await Promise.all([
    fetchProduct(req.params.id),
    fetchReviews(req.params.id, { limit: 20 }),
    fetchQnA(req.params.id),
    fetchRecommendations(req.params.id),
  ]);

  res.json({
    product: {
      ...product,
      fullDescription: product.description,
      allImages: product.images,
      videos: product.videos,
      specifications: product.specifications,
      inStock: await checkStock(product.id),
    },
    reviews: {
      average: reviews.averageRating,
      count: reviews.totalCount,
      items: reviews.items.map(r => ({
        user: r.user.name,
        rating: r.rating,
        title: r.title,
        comment: r.comment,
        helpful: r.helpfulCount,
        date: r.createdAt,
      })),
    },
    qna: qna.slice(0, 5),
    recommendations: recommendations.slice(0, 10),
  });
});

// Mobile BFF for product page
app.get('/api/mobile/products/:id', async (req, res) => {
  const product = await fetchProduct(req.params.id);

  res.json({
    id: product.id,
    name: product.name,
    price: product.price,
    discountPrice: product.discountPrice,
    thumbnail: product.images[0],
    rating: product.averageRating,
    reviewCount: product.reviewCount,
    inStock: await checkStock(product.id),
  });
});

Monitoring

Client-Specific Metrics

javascript
const promClient = require('prom-client');

const bffRequestDuration = new promClient.Histogram({
  name: 'bff_request_duration_seconds',
  help: 'BFF request duration',
  labelNames: ['client_type', 'endpoint', 'status'],
});

app.use('/api/*', (req, res, next) => {
  const start = Date.now();
  const clientType = req.path.includes('/mobile/') ? 'mobile' : 'web';

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    bffRequestDuration.observe({
      client_type: clientType,
      endpoint: req.path,
      status: res.statusCode,
    }, duration);
  });

  next();
});

Conclusion

The BFF pattern optimizes data delivery for different clients, reducing over-fetching and improving performance. Implement per-client BFFs when you have distinct frontend platforms with different data needs. Keep BFFs simple, focus on data transformation, and monitor performance. Your users will thank you for the faster, more responsive applications.

Enjoyed this essay? Share it.

Related essays

Next essay
Event-Driven Architecture Complete Guide