
Every frontend developer has seen an API return a payload that is technically correct but wrong for what they are building. You need a user's name, avatar, and their three most recent orders on the homepage. You make four separate API calls, stitch the data together in the client, and hope the network is fast enough that users do not notice the loading sequence. An endpoint returns 47 fields when you need 6, and the one field you need is named something like usr_acct_disp_nm.
This is an architecture problem. The backend was designed for general use or for a different client entirely. The Backend for Frontend (BFF) pattern exists to fix this.
The term was coined at SoundCloud and formalized by Sam Newman in his work on microservices. The core idea is simple. You build a dedicated server-side layer for each client. One generic API that tries to serve every client surface (web, mobile, TV, third-party integrations) serves none of them well.
A BFF is a server that:
The "one BFF per client surface" principle is non-negotiable. A mobile app and a web app have different data needs, performance budgets, and interaction patterns. Share a BFF between them and you are back to building a general-purpose API that optimizes for nobody.
A regular backend is concerned with business logic and data integrity. It enforces domain rules, owns the database, and exposes resources in terms the domain understands. It does not care that your homepage needs a slightly different shape of User than your profile page.
A BFF is concerned with presentation logic. It takes what the downstream backend offers and transforms it into what a specific frontend needs. In practice, that means:
The distinction is important. A backend engineer thinks in entities and invariants. The frontend engineer's frame is components and the data a screen needs. A BFF speaks that second language fluently.
Here is what the difference looks like in practice. Without a BFF, a dashboard page might do this:
// Frontend assembles the view from three separate calls
const [user, orders, notifications] = await Promise.all([
fetch("/api/users/me"),
fetch("/api/orders?userId=me&limit=3"),
fetch("/api/notifications?userId=me&unread=true"),
]);
const unreadCount = notifications.filter((n) => !n.read).length;
const displayName = `${user.firstName} ${user.lastName}`;With a BFF, the frontend gets exactly what it needs in one call:
// BFF exposes a purpose-built endpoint for this screen
const { user, recentOrders, unreadCount } = await fetch("/bff/dashboard");The aggregation and transformation logic lives in the BFF. It is not scattered across the frontend or duplicated across multiple screens.
The BFF pattern was born from the microservices world, and for good reason. When your backend is split across a dozen services (users, orders, inventory, notifications, billing), the frontend has to know about all of them. A small UI change might require coordinating with three backend teams to get a slightly different response shape.
A BFF solves this by acting as the frontend's single contract. The BFF talks to whichever downstream services it needs. If the user service changes its API, the BFF absorbs the change. The frontend does not care.
This is the anti-corruption layer pattern applied to the frontend/backend boundary. It insulates the frontend from the churn of backend evolution.
BFFs are equally useful with a monolith. The motivation just shifts. You are not orchestrating multiple services. A generic API that was not designed for your specific UI requirements is what you are dealing with.
In this context, a BFF is typically a thin adapter layer in front of your monolith. It might live in the same codebase (as a module, a set of Next.js route handlers, or a dedicated Express app inside a monorepo), or as a separate service.
The critical rule for the monolith case is to keep the BFF thin. A BFF that starts replicating business logic from the monolith is an anti-pattern. It should translate and aggregate. Business rules belong in the backend.
This is where teams most often get the pattern wrong.
The BFF should be owned by the frontend team. That is the entire point of the pattern.
If backend engineers own the BFF, it slowly drifts back toward a general-purpose API. Review cycles get slower. The frontend team ends up negotiating response shapes through pull requests and JIRA tickets. The shapes ship only when the backend team approves. You have traded one problem for a slower version of the same problem.
When frontend engineers own the BFF, they own the contract. They reshape data when they need to. They add a new endpoint without filing a request. They ship at frontend velocity.
This does require frontend engineers to write some server-side code. It is not much. BFF code is mostly glue work. Call this service, transform that response, return a clean shape. It does require comfort with HTTP, authentication flows, and async I/O. Most engineers who can write complex React applications can handle this without much trouble.
The ownership boundary is the BFF itself. The BFF calls downstream services but never owns business logic. Database writes, domain rules, and invariants stay in the backend. The BFF is firmly in the presentation layer. That distinction must be actively maintained.
Yes, it matters, especially when frontend engineers are the primary owners.
TypeScript with a Node.js runtime is my default recommendation. The reasoning is practical.
Go is a reasonable alternative when your engineering organization already has Go expertise and the BFF is expected to be maintained by full-stack or backend engineers. Go handles concurrent fan-out well, which matters when a BFF makes several downstream requests simultaneously to build a single response.
The language choice should follow the ownership model. If frontend engineers own the BFF, they should use the language they already know. Anything else creates friction, and friction eventually pushes ownership back to backend teams.
Security is one of the strongest arguments for a BFF, and it is consistently underrated.
Single-page applications have a long-standing dilemma. Where do you store the access token? localStorage is convenient but exposed to any JavaScript on the page. That is a real risk in an era of supply-chain attacks on npm packages. In-memory storage is safer, and it is gone on refresh. HTTP-only cookies work, and they require server-side handling.
A BFF solves this problem completely. The browser never holds a raw access token.
The BFF performs the token exchange. It receives the authorization code from your identity provider, exchanges it for an access and refresh token, stores the tokens server-side, and hands the browser an HTTP-only session cookie. Every subsequent request from the frontend carries that cookie. The BFF looks up the session, attaches the real token to the downstream request, and forwards it. The token is never in JavaScript-land.
// The BFF handles the OAuth callback. The browser never sees the access token
app.get("/auth/callback", async (req, res) => {
const { code } = req.query;
const tokens = await identityProvider.exchangeCode(code);
// Tokens live server-side
await sessionStore.save(req.sessionId, tokens);
res.setCookie("session", req.sessionId, {
httpOnly: true,
secure: true,
sameSite: "strict",
});
res.redirect("/dashboard");
});A BFF should never become a place where domain rules accumulate. Keep it to what it is designed for. Read aggregation, response shaping, and auth delegation.
Specifically:
If your BFF has its own database and is starting to own business rules, you have stopped building a BFF and started building a second backend. That is a different problem entirely.
If you are using Next.js, you already have the infrastructure for a lightweight BFF. That is Route Handlers (App Router) or API Routes (Pages Router). Your frontend and BFF live in the same project and deploy together as one unit.
This is the right default for small-to-medium applications. The tradeoff is that you cannot scale the BFF independently from the frontend. For most teams, that constraint is irrelevant. Start here.
For larger applications, a common pattern is a dedicated BFF service living alongside the frontend in a monorepo. Both share TypeScript types, but deploy independently. The frontend hits the BFF over HTTP. The BFF fans out to backend services.
This setup is more operationally complex but gives you independent scaling and cleaner separation. It makes sense once the BFF has enough complexity to justify the operational overhead. That means multiple aggregation patterns, caching layers, and connection pooling.
In a Kubernetes environment, the BFF can be deployed as a sidecar container in the same pod as the frontend server. The frontend communicates with the BFF over localhost, keeping latency near-zero with no external network hop. They always deploy together, which eliminates version skew between the two.
Do not share a BFF across multiple frontend applications. The moment you do, you are building an API gateway. You have reintroduced the same generalization problem you were trying to solve, one layer higher.
SoundCloud is where the pattern was formally named. Their engineering team faced exactly the coordination problem described above. Mobile and web clients needed different data shapes, and a shared generic API meant constant negotiation between frontend and backend teams. Splitting into client-specific BFFs restored shipping velocity.
Netflix runs a BFF layer for each of the devices it supports. There are a lot of them. Televisions, phones, tablets, game consoles, and web browsers have meaningfully different performance budgets and rendering constraints. A single API cannot serve all of them well. Netflix's BFF layer handles the translation for each surface.
Spotify uses a similar model. The web player, desktop client, and mobile apps each have different data requirements and different performance targets. Their BFF layer handles the fan-out to internal services and returns responses tailored to each client.
In all three cases, the BFF is a structural decision that reflects how their engineering teams are organized and how fast they need to ship.
The BFF pattern adds a network hop, a service to operate, and a deployment to manage. That overhead has to earn its place.
Skip the BFF if:
The most expensive BFF is one you built before you had a reason to. The scaffolding adds cognitive overhead, and without a real coordination problem to absorb, it becomes extra code to maintain.
The BFF pattern is a team structure tool dressed up as an architecture pattern.
It says the team building the UI should own the API contract that feeds it. The language choice (TypeScript) follows from ownership. The security model (tokens held server-side) follows from the BFF being a trusted backend layer. Deployment comes from frontend and BFF evolving together.
Use it when you have a coordination problem. That is when frontend engineers are waiting on backend teams to reshape responses, or when you are serving multiple client surfaces with meaningfully different needs. Do not use it to solve a problem you do not have yet.
When you do need it, there is nothing better for giving frontend teams full control over the contract between their code and the data it runs on.
© Melvin Laplanche - All rights reserved.