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 dataImplementation 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,priceTesting
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.