DEV Community

Munna Thakur
Munna Thakur

Posted on

BFF (Backend For Frontend): The Middle Layer That Makes Your App Fast and Clean

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

  1. What is BFF?
  2. Why Does It Exist?
  3. The Problem Without BFF
  4. How BFF Solves It
  5. When Should You Use BFF?
  6. How to Build a BFF (With Code)
  7. BFF vs API Gateway
  8. Real Companies Using BFF
  9. Tradeoffs: Is BFF Always Good?
  10. 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
Enter fullscreen mode Exit fullscreen mode

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);
    });
}, []);
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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));
}, []);
Enter fullscreen mode Exit fullscreen mode

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'])) },
    ]
  });
});
Enter fullscreen mode Exit fullscreen mode

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...) not t1 + 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→ |               |
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

❌ 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)
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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' });
  }
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. BFF is a server that lives between your frontend and your backend services. It's your frontend's dedicated helper.

  2. It reduces API calls from many to one. Your browser makes 1 request, BFF handles the rest.

  3. It shapes data for your UI. Send only what you need, nothing extra.

  4. It enables server-side caching. Repeated requests can be served instantly.

  5. It's not API Gateway. API Gateway is a generic router. BFF is UI-aware.

  6. Always call services in parallel. Promise.all() is your best friend inside BFF.

  7. 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)