๐Ÿณ IntuitiveFE
Login
โ† All concepts

Backend-for-Frontend (BFF)

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ›’ BFF = personal shopper. Instead of wandering all 7 floors yourself, your shopper fetches exactly what you need and brings it in one trip.

Without BFF โ€” the maze

Mobile app navigates 7 microservice floors. Each floor speaks a different dialect. The app must know the layout of every floor, assemble the final picture itself, and carry all the over-fetched data through a slow mobile connection.

Mobile โ†’ User Service (80ms)

Mobile โ†’ Order Service (95ms)

Mobile โ†’ Product Service (110ms)

Total: 375ms, 48KB

With BFF โ€” the personal shopper

Mobile calls the BFF once. The BFF speaks all 7 service dialects. Server-side calls are sub-millisecond on the same network. BFF returns exactly the 8 fields the mobile screen needs.

Mobile โ†’ BFF (12ms round trip)

BFF โ†’ all services in parallel (internal)

Total: 37ms, 3.2KB

BFF vs API Gateway โ€” different jobs

ConcernAPI GatewayBFF
PurposeTraffic routing, auth, rate limitingClient-specific data aggregation
Who writesPlatform / infrastructure teamFrontend team (owns the BFF)
Per clientOne gateway for all clientsSeparate BFF per client type
LogicCross-cutting: auth, logging, throttlingAggregation, transformation
OutputProxied microservice responsesClient-optimized data shape
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Mobile app makes 7 parallel API calls to different microservices. Each service returns its full object โ€” client assembles the view. High latency, massive over-fetching, N+1 waterfall on dependent calls.

Request flow

Mobileโ†’User ServiceGET /users/4280ms

Client API calls

7

Total latency

~375ms (slowest parallel chain)

Payload

48KB

Gotcha: Multiple parallel calls seem like a good idea until one service is slow โ€” the waterfall collapses to the slowest call. Timeout handling is duplicated across every client type.
Insight: The real cost is not latency alone. The client now has implicit knowledge of your service topology. Any service rename, split, or merge becomes a mobile release.
Explored:๐Ÿ”ฅ๐Ÿ›’โ—ฏ๐Ÿ”ทโšก
๐ŸŽฏ

Challenge

Step through all 5 BFF patterns. Compare the API call count and latency for each.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: What is the difference between BFF and API Gateway?

An API Gateway is infrastructure: it handles cross-cutting concerns (auth, rate limiting, SSL termination, routing) for all clients uniformly. It proxies requests to microservices without understanding the client's data needs. A BFF is application-layer: it aggregates multiple service calls into one response tailored to a specific client type (mobile, web, TV). They're often combined โ€” the gateway handles infra, the BFF handles aggregation.

Q: When does a BFF become a bottleneck?

A BFF becomes a bottleneck when: (1) it contains business logic that should live in services โ€” BFFs should aggregate, not decide; (2) it becomes a single point of failure for multiple clients; (3) it doesn't parallelize service calls โ€” sequential calls stack latencies; (4) it accumulates technical debt from multiple team ownerships. The fix: treat the BFF as a thin translation layer, keep it stateless, and move business logic to the microservices.

Q: How does tRPC eliminate the BFF layer for full-stack apps?

js
1// server/routers/homeScreen.ts
2export const homeScreenRouter = router({
3 getPageData: publicProcedure
4 .input(z.object({ userId: z.string() }))
5 .query(async ({ input, ctx }) => {
6 const [user, orders, recos] = await Promise.all([
7 ctx.prisma.user.findUnique({ where: { id: input.userId } }),
8 ctx.prisma.order.findMany({ where: { userId: input.userId }, take: 3 }),
9 getRecommendations(input.userId),
10 ]);
11 return { user, orders, recos }; // TypeScript infers the return type
12 }),
13});
14ย 
15// client/HomeScreen.tsx โ€” fully typed, zero codegen
16const { data } = trpc.homeScreen.getPageData.useQuery({ userId: '42' });
17// data.user.name โ† autocomplete works!
18// data.orders[0].total โ† type error if field doesn't exist

Bug: BFF containing business logic

js
1// โœ— BAD: BFF enforcing business rules
2app.get('/checkout', async (req, res) => {
3 const cart = await cartService.get(req.userId);
4 // BFF shouldn't decide this:
5 if (cart.totalItems > 99) throw new Error('Cart too large');
6 if (!cart.items.every(item => item.inStock)) throw new Error('Out of stock');
7 await emailService.send(req.userId, 'checkout-started');
8 // ...
9});
10ย 
11// โœ“ GOOD: BFF aggregates only
12app.get('/checkout', async (req, res) => {
13 const [cart, user, shipping] = await Promise.all([
14 cartService.get(req.userId), // validation happens in cartService
15 userService.get(req.userId),
16 shippingService.getOptions(req.userId),
17 ]);
18 res.json({ cart, user, shipping }); // shape it for the client
19});
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Sam Newman's BFF pattern origin

Sam Newman (author of Building Microservices) coined the BFF pattern in 2015 after observing that SoundCloud's API was becoming a mess of generic endpoints trying to satisfy every client type simultaneously. The insight: instead of making services know about clients, create a client-owned aggregation layer that translates the rich service API into the exact dialect each client needs.

Caching strategies within BFF

js
1// BFF can cache aggregated responses
2import { LRUCache } from 'lru-cache';
3ย 
4const cache = new LRUCache<string, unknown>({
5 max: 500,
6 ttl: 1000 * 60 * 5, // 5 minutes
7});
8ย 
9app.get('/home-screen', async (req, res) => {
10 const cacheKey = `home:${req.userId}`;
11 const cached = cache.get(cacheKey);
12 if (cached) return res.json(cached);
13ย 
14 const data = await aggregateHomeScreenData(req.userId);
15 cache.set(cacheKey, data);
16 res.json(data);
17});
18ย 
19// Cache-Aside at BFF layer:
20// User-specific: short TTL (30s)
21// Shared/catalog data: long TTL (5min) + CDN

Edge BFF with Cloudflare Workers

js
1// cloudflare-worker.ts โ€” runs in 250+ cities globally
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const url = new URL(request.url);
5 const userId = url.searchParams.get('userId')!;
6ย 
7 // Parallel origin fetches from the edge PoP
8 const [user, orders] = await Promise.all([
9 fetch(`${env.USER_SERVICE_URL}/users/${userId}`).then(r => r.json()),
10 fetch(`${env.ORDER_SERVICE_URL}/orders?user=${userId}&limit=3`).then(r => r.json()),
11 ]);
12ย 
13 return new Response(JSON.stringify({ user, orders }), {
14 headers: {
15 'Content-Type': 'application/json',
16 'Cache-Control': 'private, max-age=30',
17 },
18 });
19 },
20};

When to use each BFF approach

REST BFF

Multiple client types with very different data needs. Simple aggregation. Any language stack.

GraphQL BFF

Frontend teams want schema control. Complex data graph. Existing GraphQL investment.

tRPC BFF

Full-stack TypeScript. Next.js app. Team owns both client and server. No 3rd-party clients.

Edge BFF

Globally distributed users. Latency-sensitive UI. Light computation. Cloudflare/Vercel stack.

๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Parallelize all upstream calls in the BFF; minimize serial dependencies; cache stable data.

Gateway: cross-cutting infra (auth, rate limiting, routing). BFF: client-specific aggregation. Both are complementary.

Skip BFF when clients have identical data needs, or when the team is too small to own another service.

BFF validates the client token and issues service-to-service tokens for internal calls.

tRPC makes the server function directly callable from the client with end-to-end TypeScript types.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What does `LRUCache` in a BFF caching layer stand for, and what eviction strategy does it use?