Frontend Architecture

[Frontend Architecture] BFF Design

[Frontend Architecture] BFF Design

About this article

As the sixth installment of the “Frontend Architecture” category in the series “Architecture Crash Course for the Generative-AI Era,” this article explains BFF.

A design pattern proposed by SoundCloud in 2011 as the answer to the problem of “no one is happy with a shared API.” This article covers the BFF background, typical responsibilities, implementation patterns (Next.js API Routes/tRPC/Hono), BFF vs GraphQL, antipatterns, and AI-era type sharing - presenting the operational iron rule of “keep it thin, introduce it only when needed.”

What is BFF in the first place

BFF Implementation Pattern Selection

BFF is, roughly speaking, “an intermediate layer that provides the optimal API per client.”

Imagine a hotel concierge. For business guests, they arrange meeting rooms and taxis; for tourists, maps and restaurant reservations — delivering only the info each guest needs in the optimal form. Handing everyone the same thick guidebook (a general-purpose API) leaves most pages unused. BFF plays this concierge role, shaping and returning only the data needed for each client — Web, mobile, TV, etc.

Why BFF is needed

Without BFF, one general-purpose API would serve all clients (Web, mobile, TV), but each client has completely different screen sizes, bandwidth, and data requirements. The result: returning the same response to everyone creates an API that’s optimal for no one. The frontend discards unneeded data and supplements missing data with extra requests — inefficiency compounds, degrading both performance and security.

”Shared API” makes nobody happy

It used to be common to share “one general-purpose API” across all clients, but Web/mobile/TV each have completely different screen sizes, required data, and auth requirements, leading to the problem of “no one is happy with a shared API.” BFF solves this through “an intermediate layer that provides the optimal API per client.”

The BFF philosophy is to separate responsibilities per client.

How BFF was born

The BFF pattern was proposed around 2015 by SoundCloud engineers, then spread through adoption in mobile apps at Netflix, Spotify, Twitter, and others. The background is that “as a result of microservices proliferation,” frontends came to need to call dozens of different APIs.

Old: one general-purpose API shared by all clients
[Web]   ──┐
[Mobile]─→ [single API] ──→ [Backend]
[TV]    ──┘

The problems with this structure are three: “Web changes affect Mobile,” “each client fetches fields it doesn’t use,” and “auth requirements per client get mixed in.” Web and mobile also have different bandwidth constraints, so a 1-API share was recognized as unrealistic - and BFF was born.

Typical BFF responsibilities

The BFF ideal is “a thin server doing front-end’s grunt work.” Business logic stays in the backend services, and BFF dedicates itself to display, formatting, and proxying - that’s the healthy design.

ResponsibilityContent
API aggregationGather from multiple microservices into one screen’s worth
Data shapingConvert to a screen-friendly format, drop unneeded fields
Auth/sessionToken management, cookie-ization, hiding secret keys
CacheHTTP cache for API responses, edge cache
Rate limitDDoS countermeasures, API abuse prevention

In particular, API aggregation matters - when displaying one screen requires separately calling “user info, order history, recommendations, ads,” BFF can “parallel call” these behind the scenes and bundle them into one response. The browser doesn’t have to make 5 requests, and felt speed improves dramatically.

Typical BFF composition

The most common composition uses a full-stack FW like Next.js as the BFF layer. By placing it in the same repo as the frontend, “the same team handles UI development and API shaping.”

[Browser]
   ↓ (same-origin communication, Cookie)
[Next.js API Routes / Server Actions]  ← BFF layer
   ↓ (mTLS / internal network)
[Microservices / external APIs]

The BFF owner is the frontend team, and placing it in the same repo as the frontend is the rule. Since BFF changes alongside screen changes, separate repos balloon the change cost. Living together means “change a screen and PR the BFF in the same flow.”

Why BFF is effective

Inserting a BFF gives three concrete effects, large in scope. These can’t be obtained by simply hitting APIs directly.

1. Improved security

Hitting external APIs directly from the browser exposes API keys and auth tokens browser-side. With BFF in the middle, tokens stay only on the server side, and only an httpOnly Cookie is handed to the browser. The risk of tokens being stolen via XSS (Cross-Site Scripting, a script injection attack) is fundamentally eliminated.

2. Improved performance

You can “parallelize the calls to multiple microservices on the BFF” and bundle them into a single response. Browser-to-BFF is fast (same-origin), BFF-to-backend is fast (internal network) - “double optimization” applies.

3. Frontend dev efficiency

Because the frontend team can “decide the API shape themselves,” they can cleanly format only the data needed for the screen. The back-and-forth of asking the backend team to “add this field” disappears.

BFF implementation patterns

BFF implementation methods are varied. Framework-built-in is easy; type-safe options like tRPC are popular as the edgy choice.

PatternNotes
Next.js API Routes / Server ActionsMost prevalent in the React family
Nuxt Server RoutesEquivalent option in the Vue family
Remix Loader/ActionWeb-standards-focused design philosophy
tRPCGodlike dev experience via TypeScript type sharing
Hono + Cloudflare WorkersEdge BFF, low latency
Dedicated Express/FastifyLightweight, repo-separate from FE

With the arrival of Next.js Server Components and Server Actions, the need to “build a separate BFF” has diminished, and BFF becoming a built-in framework concern is becoming the mainstream. For simple use cases, Next.js alone is sufficient.

Type-safe BFF with tRPC

tRPC is a mechanism for fully sharing TypeScript types between server and client - rapidly spreading in recent years. Without schema definitions like REST or GraphQL, you can write APIs as if calling functions.

// server side
const appRouter = router({
  user: {
    byId: procedure.input(z.string()).query(({ input }) => ...)
  }
})

// client side - server's types apply automatically
const user = await trpc.user.byId.query("u_1")
// user's type is auto-inferred, input validated by Zod

Benefits: No schema files, editor completion via types, runtime validation (integration with Zod (a TypeScript-only schema definition library)). On TypeScript full-stack projects (FE/BE coexisting in one project), it shows overwhelming dev efficiency.

Next.js + tRPC + Zod is the strongest modern type-safe full-stack composition.

BFF vs GraphQL

GraphQL is a technology solving similar problems to BFF, and the choice between them is often confusing. The two are “not exclusive” - they can be combined - but their strengths differ, so use them for the right scenarios.

ViewpointBFFGraphQL
ImplementationEndpoints per screenSingle endpoint
Learning costLightHeavy (design/operation hard)
FlexibilityServer-side fixed shapingClient queries freely
CacheEasy via HTTP standardsRequires separate design

“When BFF fits”: few clients (Web/mobile), screen design is stable. “When GraphQL fits”: diverse clients (mobile/TV/external partners) want to query freely.

For single-client-centric use, BFF; for diverse clients querying freely, GraphQL is the rule.

Session-management standard

For auth when using BFF, the standard is “only Cookie to the browser, real tokens managed behind the BFF.” This composition maximizes both XSS resistance and token management.

1. Browser → IdP (Identity Provider, the auth foundation, e.g., Auth0) gets ID token
2. ID token sent to BFF
3. BFF verifies, issues httpOnly Session Cookie
4. Subsequent API calls are Cookie-based (browser side never touches JWT)
5. BFF manages Access Token / Refresh Token behind the scenes

The decisive benefit of this composition is never exposing JWTs to the browser. The chance of Session Cookie being stolen via XSS is eliminated by httpOnly, and even if stolen, the server can revoke immediately. Compared to designs that put JWTs in the browser, security reaches one rank higher.

BFF + httpOnly Cookie is the safest composition for modern Web auth.

BFF antipatterns

BFF is convenient, but if you don’t keep it appropriately thin, it bloats rapidly into new debt. Without grasping these typical failure patterns, the BFF itself becomes another monolith.

  • Bloating: business logic flows into the BFF and duplicates with core services. Fixing requires changing both
  • Siloization: each BFF starts its own user management or cache, and consistency is lost
  • Cache placement problem: confusion about where (in BFF / in backend / on CDN) and how long to cache
  • Auth duplication: each BFF independently implements auth, and auth bugs sprout everywhere

BFF is a thin layer for the frontend. Bringing in business logic duplicates with core services and breaks down.

”The day BFF became the main system” (industry stories)

There’s a story told at study groups about a site that piled discount calculations and inventory reservations into Next.js API Routes “because the backend team was slow to respond” - resulting in a reverse structure a year later: the BFF effectively became the main system, and the real Backend became a CRUD storage. Fixing required changing both, and they exhausted themselves on dual maintenance.

The scene of “let’s just write it quickly in Next.js” producing a horrifying reversal six months later is witnessed at many sites. The scary part of this accident: at the individual PR level, it was just “writing a little”. A one-line discount calculation became inventory reservation in a month, point calculation in six months, and “most of the core logic” in a year - the typical pattern.

BFF “looks harmless at the entrance, but always bloats if left alone.” The lesson “don’t put a vault in the entrance lobby” was born from accumulating these accidents.

The iron rule for BFF is keep it thin. Pound on the early signs of business logic creeping in.

Phased BFF adoption decision matrix

The most important judgment for BFF is whether to introduce it, and it auto-derives from scale and architecture. Here’s the practical phased table.

PhaseScale/architectureBFF treatmentImplementation method
1. Personal/MVP~1,000 MAUUnneeded (direct API calls)fetch + useState
2. Early startup~10,000 MAU, single backendThin BFF (FW built-in)Next.js API Routes / Server Actions
3. Mid-size SaaS10,000-100,000 MAU, multiple microservicesAggregation BFFtRPC + Next.js
4. Large100,000+ MAU, multi-teamDedicated BFF layer (per screen)Dedicated Express/Fastify + gRPC
5. Multi-clientWeb + Mobile + TVPer-client BFFEach client dedicated
6. Edge-distribution priorityGlobal, low-latency-focusedEdge BFFHono + Cloudflare Workers

“The substantive lower bound for adopting BFF is when microservices + aggregation across 3+ services is required.” Below that, the thin layer of Next.js API Routes / Server Actions is enough; spinning up a dedicated BFF server is over-design.

BFF goes in when needed. Adopting “for the future” backfires on operational costs.

BFF design’s pitfalls and forbidden moves

Here are the typical accidents in BFF operation. All of them are entry points to BFF becoming the main system.

Forbidden moveWhy it’s bad
Write business logic in BFFDuplicates with core service in a year. Don’t put a vault in the entrance lobby
BFF owned by the backend teamEvery screen change requires cross-team coordination. Frontend team ownership is the rule
Separate repos for BFF and FEScreen change requires 2 PRs, accidents at sync timing
No cache strategy in BFFOrigin load concentrates, BFF becomes the bottleneck
Re-implementing auth in BFF (each BFF independently)Auth bugs sprout everywhere. Lean on unified auth
Connect BFF and FE with REST + hand-typed typesTypes are split, dual-managed type definitions. Fully share via tRPC
No timeout in BFFBackend failures topple the BFF, avalanche cascade
Update without optimistic locking in BFFConcurrent updates cause lost updates, data corruption
Letting non-frontend clients hit the BFF tooThe point of BFF disappears. Maintain use-case specialization
Unify multiple microfrontends on one BFFTeam independence is lost. One BFF per frontend
Adopting BFF early as “more modern even at small scale”Over-design. Server Actions are enough at that scale — dedicated BFF just adds ops burden
Confusing “BFF and GraphQL are the same”Opposite philosophies. BFF for few clients, GraphQL for many clients with free queries

“As of 2024 the standard is Next.js Server Actions + tRPC.” Fetch with Server Components, update with Server Actions, share types with tRPC. With this triad, “BFF as a separate layer” can be implemented without consciousness of it. Traditional separate-server BFFs are considered only when there are 5+ microservices and multi-team parallel development.

BFF is the entrance lobby. The moment you bring in business logic, it becomes a second monolith.

AI decision axes

Favored in the AI eraDisfavored in the AI era
tRPC (full type sharing)Hand-written REST + dual-managed type definitions
Next.js Server Actions (self-contained in one file)BFF in a separate repo, scattered specs
Validation via Zod schemasHand-written string verification
Same repo with FE/BE coexistingSeparated repos, distant specs
  1. Judge necessity from scale (small scale: not needed; microservices: required)
  2. Prefer framework built-ins (Next.js API Routes / Server Actions)
  3. Type sharing + same repo (tRPC + Zod is strongest)
  4. Keep BFF thin (business logic stays in core services)

tRPC + Zod combination maximizes AI generation accuracy

tRPC automatically shares types between frontend and backend, and when validation is defined with Zod schemas, API input/output types automatically propagate to the frontend. For AI, “the list of available APIs and each parameter’s types” are explicit in the codebase, so API-call generation accuracy rises dramatically.

With hand-written REST APIs + dual-maintained type definitions, when frontend types and server implementation diverge, AI can’t judge which to trust and risks generating inaccurate code.

What to decide - what is your project’s answer?

For each of the following, try to articulate your project’s answer in 1-2 sentences. Starting work with these vague always invites later questions like “why did we decide this again?”

  • Whether to set up a BFF (judge from scale and architecture)
  • Implementation tech (Next.js / tRPC / Hono / dedicated server)
  • Repo composition (coexist with FE / separate repo)
  • How to handle auth (Cookie / Bearer / Session)
  • Cache strategy (in BFF / CDN / which has priority)
  • Logging and monitoring (correlation IDs, OpenTelemetry)

https://en.senkohome.com/arch-intro-frontend-overview/ https://en.senkohome.com/arch-intro-frontend-framework/ https://en.senkohome.com/arch-intro-index-frontend/

Summary

This article covered BFF, including responsibilities, implementation patterns, differences with GraphQL, antipatterns, and type sharing.

Keep BFF thin, judge necessity from scale, and maximize AI productivity through type sharing + same repo. That is the practical answer for BFF design in 2026.

Next time we’ll cover authentication and authorization (frontend side) (Cookie/JWT storage, XSS/CSRF countermeasures).

Back to series TOC -> ‘Architecture Crash Course for the Generative-AI Era’: How to Read This Book

I hope you’ll read the next article as well.

📚 Series: Architecture Crash Course for the Generative-AI Era (36/89)