Architectural pattern where each client type has its own dedicated backend adapting microservice APIs to that client's specific needs.
Backend for Frontend (BFF) is an architectural pattern where each client type — web, mobile, IoT, desktop applications — has its own dedicated backend. Instead of forcing all clients to consume the same generic APIs, each BFF acts as an adaptation layer that aggregates, transforms, and optimizes data specifically for its client's needs.
The pattern emerges from the practical reality that different clients have different data access patterns, network constraints, processing capabilities, and user experiences. A web application might need rich, detailed data, while a mobile app requires lighter responses optimized for slow connections. An IoT device might need only a minimal subset of data in binary format.
Each BFF is owned by the corresponding frontend team, enabling greater autonomy and development velocity. This contrasts with a centralized API Gateway that attempts to serve all clients with a single interface.
The most common pattern is creating a dedicated BFF for each client platform:
// Web BFF - Optimized for web applications
app.get('/api/dashboard', async (req, res) => {
const [user, analytics, notifications, settings] = await Promise.all([
userService.getProfile(req.userId),
analyticsService.getDashboardData(req.userId),
notificationService.getRecent(req.userId, 10),
settingsService.getUserPreferences(req.userId)
]);
// Rich aggregation for web with multiple widgets
res.json({
user: {
name: user.fullName,
avatar: user.profileImage,
role: user.role
},
dashboard: {
metrics: analytics.summary,
charts: analytics.chartData,
notifications: notifications.map(n => ({
id: n.id,
message: n.content,
timestamp: n.createdAt,
priority: n.priority
})),
quickActions: settings.enabledActions
}
});
});
// Mobile BFF - Optimized for mobile devices
app.get('/api/dashboard', async (req, res) => {
const [user, criticalNotifications] = await Promise.all([
userService.getProfile(req.userId),
notificationService.getCritical(req.userId, 3)
]);
// Lightweight response for mobile
res.json({
user: user.displayName,
avatar: user.thumbnailImage,
alerts: criticalNotifications.length,
hasUpdates: criticalNotifications.length > 0
});
});GraphQL can act as a natural BFF, allowing each client to request exactly the data it needs:
# GraphQL schema as BFF
type Query {
dashboard(platform: Platform!): DashboardData
}
type DashboardData {
user: User
metrics: [Metric]
notifications: [Notification]
settings: UserSettings
}
enum Platform {
WEB
MOBILE
TABLET
}// Resolver that adapts based on platform
const resolvers = {
Query: {
dashboard: async (_, { platform }, { userId }) => {
const baseData = await getDashboardData(userId);
switch (platform) {
case 'WEB':
return {
...baseData,
metrics: baseData.metrics, // Complete data
notifications: baseData.notifications.slice(0, 10)
};
case 'MOBILE':
return {
user: baseData.user,
metrics: baseData.metrics.filter(m => m.priority === 'HIGH'),
notifications: baseData.notifications.slice(0, 3)
};
default:
return baseData;
}
}
}
};The BFF pattern adds unnecessary complexity in certain scenarios:
| Aspect | BFF | API Gateway | GraphQL |
|---|---|---|---|
| Purpose | Adapt data per client | Route and protect | Flexible querying |
| Ownership | Frontend team | Platform team | Shared |
| Business logic | Aggregation and transformation | Routing and auth | Data resolution |
| Quantity | One per client type | One centralized | One with multiple queries |
| Complexity | Medium-High | Low-Medium | Medium |
| Flexibility | High per client | Low | Very high |
| Network overhead | Optimized | Variable | Optimized |
// ❌ Anti-pattern: BFF that only forwards calls
app.get('/api/users/:id', (req, res) => {
// Just proxy, no added value
return userService.getUser(req.params.id);
});// ✅ Correct pattern: BFF that adds value
app.get('/api/users/:id', async (req, res) => {
const [user, permissions, preferences] = await Promise.all([
userService.getUser(req.params.id),
authService.getUserPermissions(req.params.id),
settingsService.getPreferences(req.params.id)
]);
return {
profile: {
name: user.fullName,
email: user.email,
avatar: user.profileImage
},
capabilities: permissions.map(p => p.action),
settings: preferences.ui
};
});BFFs should be limited to data aggregation and transformation, not implement complex business logic that should reside in the underlying microservices.
A BFF can coexist with other architectural patterns:
The BFF pattern solves the fundamental problem of generic APIs trying to serve multiple clients with divergent needs. In organizations with autonomous frontend teams, it enables each team to optimize their backend for specific access patterns without impacting other clients. This reduces latency, improves user experience, and accelerates development by eliminating dependencies between teams. However, it introduces operational complexity — each BFF must be deployed, monitored, and maintained independently, requiring careful consideration of the tradeoff between team autonomy and infrastructure overhead.
Architectural style structuring an application as a collection of small, independent, deployable services, each with its own business logic and data.
Principles and practices for designing clear, consistent, and evolvable programming interfaces that facilitate integration between systems.
Pattern providing a single entry point for multiple microservices, handling routing, authentication, rate limiting, and response aggregation.
Architectural pattern extending microservices to the frontend, allowing independent teams to develop and deploy parts of a web application autonomously.
Architectural pattern where components communicate through asynchronous events, enabling decoupled, scalable, and reactive systems.