Do I Need a Backend for Frontend (BFF)?
Do I Need a Backend for Frontend (BFF)?
The Problem
Your frontend talks to 5 microservices. Each request:
- Hits multiple endpoints
- Transforms data shapes
- Handles auth differently
- Returns more data than you need
Should you add a BFF layer, or is it over-engineering?
What is a BFF?
A thin backend that sits between your frontend and your services:
Without BFF:
┌────────┐ ┌──────────────┐
│ │────▶│ User Service │
│ │ └──────────────┘
│ React │ ┌──────────────┐
│ App │────▶│ Order Service│
│ │ └──────────────┘
│ │ ┌──────────────┐
└────────┘────▶│ Product Svc │
└──────────────┘
With BFF:
┌────────┐ ┌─────┐ ┌──────────────┐
│ │ │ │────▶│ User Service │
│ React │────▶│ BFF │────▶│ Order Service│
│ App │ │ │────▶│ Product Svc │
└────────┘ └─────┘ └──────────────┘
When You Need a BFF
1. Multiple API Calls Per Page
// ❌ Client makes 4 requests, user waits for slowest
async function DashboardPage() {
const [user, orders, notifications, recommendations] = await Promise.all([
fetch('/api/users/me'),
fetch('/api/orders?limit=5'),
fetch('/api/notifications?unread=true'),
fetch('/api/recommendations'),
]);
// ...
}
// ✅ BFF aggregates into one request
// app/api/dashboard/route.ts
export async function GET() {
const [user, orders, notifications, recommendations] = await Promise.all([
userService.getCurrentUser(),
orderService.getRecent(5),
notificationService.getUnread(),
recommendationService.get(),
]);
return Response.json({
user: { name: user.name, avatar: user.avatar },
orders: orders.map(o => ({ id: o.id, total: o.total, status: o.status })),
unreadCount: notifications.length,
recommendations: recommendations.slice(0, 4),
});
}
Benefit: One round trip instead of four. BFF makes parallel calls server-side (faster, no CORS).
2. Over-Fetching from Generic APIs
// Generic /api/users/123 returns everything
{
"id": "123",
"email": "user@example.com",
"name": "John",
"passwordHash": "...", // Don't expose this!
"createdAt": "...",
"updatedAt": "...",
"settings": { ... }, // 50 fields you don't need
"internalFlags": { ... },
"auditLog": [ ... ] // 1000 entries
}
// BFF shapes it for the UI
{
"id": "123",
"name": "John",
"avatar": "https://..."
}
3. Different Clients Need Different Data
┌──────────────┐ ┌─────────────┐
│ Web App │────▶│ Web BFF │──┐
└──────────────┘ └─────────────┘ │ ┌──────────────┐
├───▶│ Services │
┌──────────────┐ ┌─────────────┐ │ └──────────────┘
│ Mobile App │────▶│ Mobile BFF │──┘
└──────────────┘ └─────────────┘
Mobile needs different data shapes, pagination, image sizes. Each BFF optimizes for its client.
4. Auth Token Exchange
// Client has session cookie → BFF exchanges for service tokens
// app/api/orders/route.ts
export async function GET(request: Request) {
// 1. Verify user session (cookie-based)
const session = await getSession(request);
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
// 2. Get service-to-service token (client never sees this)
const serviceToken = await getServiceToken('order-service');
// 3. Call internal service
const orders = await fetch('http://order-service.internal/orders', {
headers: { Authorization: `Bearer ${serviceToken}` },
});
return Response.json(await orders.json());
}
Benefit: Internal service tokens never exposed to browser.
When You DON'T Need a BFF
1. Single Backend / Monolith
┌────────┐ ┌────────────────┐
│ React │────▶│ Your API │
│ App │ │ (already BFF) │
└────────┘ └────────────────┘
If you control the backend and it's already shaped for your frontend, you have a BFF.
2. GraphQL
// GraphQL already solves aggregation and over-fetching
const { data } = useQuery(gql`
query Dashboard {
me {
name
avatar
}
orders(limit: 5) {
id
total
}
unreadNotificationCount
}
`);
GraphQL IS your BFF layer. Adding another BFF is redundant.
3. Simple CRUD Apps
If you're just reading/writing to one database with simple transforms, you don't need the extra layer.
The Next.js Answer: You Already Have a BFF
app/
├── api/
│ ├── dashboard/route.ts ← This is your BFF
│ ├── checkout/route.ts
│ └── user/route.ts
Next.js API routes + Server Components = built-in BFF pattern.
// app/dashboard/page.tsx — Server Component IS the BFF
async function DashboardPage() {
// These run on server, not client
const user = await db.user.findUnique({ where: { id: session.userId } });
const orders = await orderService.getRecent(user.id, 5);
const stats = await analyticsService.getDashboardStats(user.id);
// Only send what UI needs
return (
<Dashboard
user={{ name: user.name, avatar: user.avatar }}
orders={orders.map(o => ({ id: o.id, total: o.total }))}
stats={stats}
/>
);
}
Decision Tree
Do you control the backend API?
├─ YES, and it's shaped for your frontend
│ └─ You don't need a separate BFF
├─ YES, but it's a generic/shared API
│ └─ Consider reshaping it OR add BFF
└─ NO, it's microservices / third-party APIs
├─ Using GraphQL?
│ └─ GraphQL is your BFF
└─ Using REST?
└─ BFF will help aggregate + transform
Implementation Options
| Approach | When to Use |
|---|---|
| Next.js API Routes | Already using Next.js |
| Next.js Server Components | Data fetching for pages |
| Standalone Node.js BFF | Separate deployment needed |
| Edge Functions (Vercel/Cloudflare) | Low latency, simple transforms |
| GraphQL Gateway | Multiple GraphQL services |
TL;DR
Use a BFF when:
- Frontend calls multiple services per page
- APIs return too much data
- You need to hide internal auth/tokens
- Different clients need different shapes
Skip the BFF when:
- You control a backend already shaped for your UI
- You're using GraphQL
- It's a simple CRUD app
In Next.js: Server Components + API Routes ARE your BFF. You probably already have one.
What did you think?