You have a React app. It's calling 5 APIs. Your UI is slow, your code is messy, and your users are leaving. There's a pattern that solves all of this — and it's called BFF.
Table of Contents
- What is BFF?
- Why Does It Exist?
- The Problem Without BFF
- How BFF Solves It
- When Should You Use BFF?
- How to Build a BFF (With Code)
- BFF vs API Gateway
- Real Companies Using BFF
- Tradeoffs: Is BFF Always Good?
- Key Takeaways
What is BFF?
BFF = Backend For Frontend
It is a server layer that sits between your frontend and your backend services.
Frontend (React / Mobile App)
↓
BFF Layer (Node.js / GraphQL)
↓
Backend Microservices + Databases
Think of it like this:
You go to a restaurant. You don't walk into the kitchen and talk to the chef, the prep cook, the dishwasher, and the manager separately. You talk to one waiter. The waiter goes to the kitchen, coordinates everything, and brings your food.
The waiter = BFF. The kitchen staff = your backend services.
Why Does It Exist?
Modern apps are built with microservices. Instead of one big server, you have many small ones:
- User Service
- Product Service
- Recommendation Service
- Payment Service
- Notification Service
This is great for the backend team. But it creates a big problem for the frontend.
The Problem Without BFF
Let's say you're building a Netflix-style homepage. Your frontend needs to show:
- Trending movies
- Top-rated shows
- Personalized recommendations
- Continue watching list
- New releases
Without BFF, your React app calls all these APIs separately:
// 😵 Messy Frontend Code
useEffect(() => {
const trending = fetch('/api/trending');
const topRated = fetch('/api/top-rated');
const recommended = fetch('/api/recommended');
const continueWatching = fetch('/api/continue-watching');
const newReleases = fetch('/api/new-releases');
Promise.all([trending, topRated, recommended, continueWatching, newReleases])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([t, tr, r, c, n]) => {
setTrending(t);
setTopRated(tr);
setRecommended(r);
setContinueWatching(c);
setNewReleases(n);
});
}, []);
Problems This Creates
1. Too Many Network Requests
Every request has a cost:
- DNS lookup (finding the server's address)
- TCP connection (opening a line of communication)
- TLS handshake (security check for HTTPS)
- Sending headers
- Waiting for response
5 API calls = 5x that overhead. On a slow mobile connection, this kills your app.
2. Browser Connection Limits
Browsers allow only ~6 parallel connections per domain. If you make 10 API calls to the same domain, 4 of them are forced to wait in a queue.
API 1 ✅ Running
API 2 ✅ Running
API 3 ✅ Running
API 4 ✅ Running
API 5 ✅ Running
API 6 ✅ Running
API 7 ⏳ Waiting in queue...
API 8 ⏳ Waiting in queue...
3. Over-fetching Data
Your User Service returns 30 fields. Your frontend only needs 3. You're downloading data you'll never use — wasting bandwidth on every single request.
// ❌ What backend returns (too much)
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2020-01-01",
"updatedAt": "2024-06-01",
"internalScore": 98,
"adminNotes": "...",
"billingInfo": { ... },
"preferences": { ... }
}
// ✅ What frontend actually needs
{
"name": "John Doe",
"email": "john@example.com"
}
4. Heavy, Complex Frontend Code
Your React component is now responsible for:
- Fetching 5 APIs
- Waiting for all of them
- Handling each one's errors
- Combining the data
- Feeding it to the UI
That's too much responsibility for a component whose job is to display things.
5. Multiple Re-renders
In React, every setState() call triggers a re-render. 5 API calls = potentially 5 re-renders = janky UI.
How BFF Solves It
With a BFF, your frontend makes one single request:
// ✅ Clean Frontend Code
useEffect(() => {
fetch('/api/homepage')
.then(res => res.json())
.then(data => setPageData(data));
}, []);
And your BFF server handles all the complexity:
// BFF Layer (Node.js + Express)
app.get('/api/homepage', async (req, res) => {
// Call all backend services IN PARALLEL on the server
const [trending, topRated, recommended, continueWatching, newReleases] =
await Promise.all([
getTrending(),
getTopRated(),
getRecommended(req.user.id),
getContinueWatching(req.user.id),
getNewReleases()
]);
// Shape the data — only send what the UI needs
res.json({
hero: trending[0],
rows: [
{ title: "Trending Now", items: trending.map(pick(['id', 'title', 'thumbnail'])) },
{ title: "Top Rated", items: topRated.map(pick(['id', 'title', 'thumbnail'])) },
{ title: "Recommended", items: recommended.map(pick(['id', 'title', 'thumbnail'])) },
{ title: "Continue Watching", items: continueWatching.map(pick(['id', 'title', 'progress'])) },
{ title: "New Releases", items: newReleases.map(pick(['id', 'title', 'thumbnail'])) },
]
});
});
Why is This Faster?
| Factor | Without BFF | With BFF |
|---|---|---|
| Requests from browser | 5 requests | 1 request |
| Network overhead | 5× DNS, TCP, TLS | 1× DNS, TCP, TLS |
| Browser queue bottleneck | Yes (can happen) | No |
| Data payload | Over-fetched | Exactly what's needed |
| React re-renders | 5 (one per API) | 1 |
| Frontend complexity | High | Low |
Server-side aggregation is faster because:
- Servers have faster internal networks than public internet
- No browser connection limits on the server
- Parallel calls on the server complete in
max(t1, t2, t3...)nott1 + t2 + t3
When Should You Use BFF?
Use BFF when you have one or more of these situations:
✅ Use BFF When:
1. Multiple clients with different data needs
Your web app, iOS app, and Android app all need different data shapes from the same backend. Instead of changing your backend for each, create a BFF per client:
Web Frontend → Web BFF → | |
iOS App → iOS BFF → | Microservices |
Android App → Android BFF→ | |
2. Too many API calls from the frontend
If your page loads need 4+ API calls to render, BFF reduces that to 1.
3. You have microservices
BFF is the perfect companion to a microservices architecture. It handles the "orchestration" so frontends don't have to.
4. Your frontend is too complex
If your React components have 100+ lines just for fetching and combining data, BFF can clean that up dramatically.
5. Performance matters
BFF enables caching on the server side. Once you fetch and shape the homepage data, you can cache it for 60 seconds. Every user in that window gets a near-instant response.
// Caching in BFF
app.get('/api/homepage', async (req, res) => {
const cacheKey = 'homepage-data';
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
// Fetch, shape, cache, respond
const data = await fetchAndShapeHomepageData();
await redis.set(cacheKey, JSON.stringify(data), 'EX', 60); // 60 second cache
res.json(data);
});
❌ Skip BFF When:
- You have a simple app with 1–2 API calls
- You have a single client (web only, no mobile)
- Your backend already returns perfectly shaped data
- You don't have a team to maintain the extra service
How to Build a BFF (With Code)
Here's a minimal working BFF for a product page:
Folder Structure
project/
├── frontend/ → React App
│ └── src/
│ └── HomePage.jsx
├── bff/ → BFF Server
│ ├── index.js
│ ├── routes/
│ │ └── homepage.js
│ └── services/
│ ├── productService.js
│ ├── reviewService.js
│ └── userService.js
└── backend/ → Microservices (separate repos ideally)
BFF Server (bff/index.js)
const express = require('express');
const homepageRouter = require('./routes/homepage');
const app = express();
app.use(express.json());
app.use('/api', homepageRouter);
app.listen(4000, () => console.log('BFF running on port 4000'));
Route Handler (bff/routes/homepage.js)
const { getProducts } = require('../services/productService');
const { getReviews } = require('../services/reviewService');
const { getUser } = require('../services/userService');
router.get('/homepage', async (req, res) => {
try {
// All calls run in PARALLEL
const [products, reviews, user] = await Promise.all([
getProducts(),
getReviews(),
getUser(req.headers['user-id'])
]);
// Shape for the UI
res.json({
greeting: `Welcome back, ${user.firstName}!`,
featured: products.slice(0, 5).map(p => ({
id: p.id,
name: p.name,
price: p.price,
image: p.thumbnailUrl // only what we need, not all 20 fields
})),
topReviews: reviews.filter(r => r.rating >= 4).slice(0, 3)
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Something went wrong' });
}
});
React Frontend (frontend/src/HomePage.jsx)
// ✅ Super clean — no business logic here
function HomePage() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('http://localhost:4000/api/homepage')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <Skeleton />;
return (
<div>
<h1>{data.greeting}</h1>
<ProductRow items={data.featured} />
<ReviewSection reviews={data.topReviews} />
</div>
);
}
Notice how clean the component is. It has one job: display what the BFF gives it.
BFF vs API Gateway
These are often confused. Here's the clear difference:
| API Gateway | BFF | |
|---|---|---|
| Purpose | Routing, auth, rate limiting | UI-specific data shaping |
| Who uses it | All clients (web, mobile, 3rd party) | One specific client |
| Knows about UI? | No | Yes |
| Data shaping | Minimal | Heavy |
| Example tools | Kong, AWS API Gateway | Node.js, GraphQL/Apollo |
In large systems, you often have both:
Client → API Gateway (auth + routing) → BFF (data shaping) → Microservices
Real Companies Using BFF
Netflix
Netflix has separate BFF layers for different devices (TV, browser, mobile). Each device needs different data, thumbnail sizes, and video quality options. A single backend can't know all of that — so each device gets its own BFF.
Meta (Facebook)
Meta uses GraphQL — which is essentially a supercharged BFF. The frontend queries exactly the data it needs, and the GraphQL layer resolves it from multiple backend services.
SoundCloud
SoundCloud was one of the first companies to formally describe the BFF pattern in 2015. They created separate backends for their web and mobile clients because the data needs were so different.
Tradeoffs: Is BFF Always Good?
No pattern is perfect. Here's the honest picture:
✅ Pros
- Cleaner, simpler frontend code
- Faster perceived performance (fewer round trips)
- Easy to cache at the BFF layer
- Each team (web, mobile) can own their BFF
- Better security (hide internal service structure)
⚠️ Cons
- One more service to deploy and maintain
- One more thing that can fail (your BFF can become a bottleneck)
- Can lead to code duplication if web BFF and mobile BFF share logic
- Adds latency if done wrong (if BFF calls are sequential, not parallel)
The key rule: always call backend services in parallel inside your BFF using Promise.all(). Never await them one by one.
// ❌ BAD — Sequential, slow
const products = await getProducts(); // wait 200ms
const reviews = await getReviews(); // wait 150ms
const user = await getUser(id); // wait 100ms
// Total: 450ms
// ✅ GOOD — Parallel, fast
const [products, reviews, user] = await Promise.all([
getProducts(), // \
getReviews(), // > all run at the same time
getUser(id) // /
]);
// Total: 200ms (just the slowest one)
Key Takeaways
BFF is a server that lives between your frontend and your backend services. It's your frontend's dedicated helper.
It reduces API calls from many to one. Your browser makes 1 request, BFF handles the rest.
It shapes data for your UI. Send only what you need, nothing extra.
It enables server-side caching. Repeated requests can be served instantly.
It's not API Gateway. API Gateway is a generic router. BFF is UI-aware.
Always call services in parallel.
Promise.all()is your best friend inside BFF.Use it when you have multiple clients or complex data aggregation. Skip it for simple apps.
One-Line to Remember
BFF = One smart server that talks to many backend services, so your frontend doesn't have to.
If this helped you, consider sharing it. If you have questions, drop them in the comments below — happy to go deeper on any section!
Tags: #webdev #systemdesign #javascript #react #node #architecture #backend #frontend #performance
Top comments (0)