Back to blog

BFF Pattern: Backend for Frontend Explained

architecturenextjsbackendweb-developmentsystem design
BFF Pattern: Backend for Frontend Explained

You've built a clean REST API for your product. Then the mobile team says the dashboard endpoint returns too much data. The web team says they need to combine three separate API calls into one. And the TV app team says the whole data shape is wrong for their use case.

Your single, "universal" API is starting to crack under pressure from multiple clients — each with different needs.

This is the problem the Backend for Frontend (BFF) pattern was designed to solve.

What You'll Learn

✅ What the BFF pattern is and the problem it solves
✅ How BFF differs from a general-purpose API
✅ When to use BFF and when not to
✅ How Next.js naturally implements the BFF pattern
✅ Practical examples with Server Components and Route Handlers


Part 1: The Problem — One API, Many Clients

Modern applications rarely serve a single client. You might have:

  • A web app with complex dashboards needing lots of data
  • A mobile app on a slow network needing minimal, optimized payloads
  • A smart TV app with a completely different UI structure
  • A third-party integration consuming your data in its own way

The Pain Points

1. Over-fetching / Under-fetching

Each client gets the same data shape, even if it doesn't need it. The mobile app fetches a 50-field user object but only renders 5 fields. The web app needs data from three endpoints and must make three round trips.

2. Tight Coupling

When you change the API to satisfy the web app, you risk breaking the mobile app. Every client is dependent on the same contract.

3. Aggregation Logic Leaks Into the Frontend

The web app ends up calling /users, /orders, and /notifications separately, then assembling the result in JavaScript on the client. That aggregation logic belongs on the server.

4. Performance Mismatch

A mobile client on a 3G connection has very different latency tolerances than a server-side rendered web app. One API design can't optimally serve both.


Part 2: The BFF Pattern

Backend for Frontend (BFF) is an architectural pattern where you create a dedicated backend service for each frontend client. Each BFF is owned and shaped by the frontend team it serves.

The term was coined by Sam Newman (author of Building Microservices) and popularized at SoundCloud.

What a BFF Does

A BFF sits between the frontend and your downstream services (microservices, databases, third-party APIs). It:

  • Aggregates data from multiple services into a single response
  • Transforms data into the exact shape the frontend needs
  • Filters fields the client doesn't need (reduces payload size)
  • Orchestrates complex sequences of service calls
  • Applies client-specific auth logic and rate limiting

BFF vs General API

ConcernGeneral APIBFF
Owned byBackend teamFrontend team (or full-stack)
Data shapeGeneric, reusableTailored to one client
AggregationNone (client does it)Done server-side
ClientsManyOne
Change velocitySlow (breaking changes)Fast (co-evolves with UI)
VersioningComplexSimpler (1:1 with frontend)

Part 3: When to Use BFF

Good Fit

  • Multiple frontends with meaningfully different data needs
  • Microservices backend — clients shouldn't orchestrate many service calls
  • Mobile apps where payload size and latency matter significantly
  • Full-stack teams who own both the frontend and its BFF
  • Third-party APIs that you want to normalize behind a stable interface

Poor Fit

  • Single client — if you only have one frontend, a BFF adds overhead without benefit
  • Simple CRUD apps — if your data model maps cleanly to your UI, no translation layer is needed
  • Small teams — maintaining separate BFFs is infrastructure cost; use it when it solves a real pain

Rule of thumb: Start with a single API. Introduce a BFF when you notice frontend teams spending significant effort to aggregate or transform data that could be done once on the server.


Part 4: Next.js as a Natural BFF

Here's where it gets practical. Next.js is, architecturally, a BFF.

When you build a Next.js application, you get:

  1. React Server Components — components that run on the server, fetch data directly, and send only the HTML/data the client needs
  2. Route Handlers (/app/api/...) — server-side API endpoints co-located with your frontend
  3. Server Actions — server functions called directly from client components

Together, these features let your Next.js app act as the BFF layer between your browser and your downstream data sources.

Server Components as BFF Aggregation

Before React Server Components, data fetching happened in the browser. The browser called multiple APIs and assembled the result. Now that logic runs on the server.

Old pattern (client-side aggregation):

// ❌ Client fetches data from multiple endpoints
'use client'
 
export default function Dashboard() {
  const [user, setUser] = useState(null)
  const [orders, setOrders] = useState([])
 
  useEffect(() => {
    // Three round trips from the browser
    fetch('/api/users/me').then(r => r.json()).then(setUser)
    fetch('/api/orders?limit=5').then(r => r.json()).then(setOrders)
  }, [])
 
  return <DashboardView user={user} orders={orders} />
}

BFF pattern with Server Components:

// ✅ Server Component fetches and aggregates — zero client round trips
import { getUser } from '@/lib/user'
import { getRecentOrders } from '@/lib/orders'
 
export default async function Dashboard() {
  // Both run on the server, in parallel
  const [user, orders] = await Promise.all([
    getUser(),
    getRecentOrders({ limit: 5 }),
  ])
 
  // Only the rendered HTML is sent to the browser
  return <DashboardView user={user} orders={orders} />
}

The server component is the BFF. It fetches from your database or downstream APIs, aggregates, filters, and sends only what the UI needs.

Route Handlers as BFF Endpoints

For client-driven data needs (search, mutations, real-time updates), Next.js Route Handlers give you BFF-style API endpoints that live next to your frontend code.

app/
├── api/
│   ├── user/
│   │   └── route.ts       ← BFF endpoint for user data
│   ├── dashboard/
│   │   └── route.ts       ← Aggregates user + orders + notifications
│   └── search/
│       └── route.ts       ← Calls search service, transforms results
├── dashboard/
│   └── page.tsx

A Route Handler that acts as a BFF aggregation layer:

// app/api/dashboard/route.ts
import { NextResponse } from 'next/server'
import { getUser } from '@/lib/user'
import { getOrders } from '@/lib/orders'
import { getNotifications } from '@/lib/notifications'
import { getSession } from '@/lib/auth'
 
export async function GET() {
  const session = await getSession()
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
 
  // Aggregate from multiple sources in parallel
  const [user, orders, notifications] = await Promise.all([
    getUser(session.userId),
    getOrders(session.userId, { limit: 5 }),
    getNotifications(session.userId, { unreadOnly: true }),
  ])
 
  // Shape the response for exactly what the dashboard needs
  return NextResponse.json({
    user: {
      name: user.name,
      avatar: user.avatarUrl,
    },
    recentOrders: orders.map(o => ({
      id: o.id,
      status: o.status,
      total: o.totalAmount,
      date: o.createdAt,
    })),
    unreadCount: notifications.length,
  })
}

The mobile app might have a completely different Route Handler at /api/mobile/dashboard that returns a leaner payload — a separate BFF for a separate client.

Server Actions as BFF Mutations

Server Actions are another dimension: they let client components call server-side logic (mutations, form submissions) without a separate API endpoint.

// app/actions/order.ts
'use server'
 
import { createOrder } from '@/lib/orders'
import { sendConfirmationEmail } from '@/lib/email'
import { revalidatePath } from 'next/cache'
 
export async function placeOrder(formData: FormData) {
  const items = JSON.parse(formData.get('items') as string)
 
  // Orchestrate: create order + send email + invalidate cache
  const order = await createOrder(items)
  await sendConfirmationEmail(order)
  revalidatePath('/orders')
 
  return { orderId: order.id }
}
// app/checkout/page.tsx
'use client'
import { placeOrder } from '@/app/actions/order'
 
export default function Checkout() {
  return (
    <form action={placeOrder}>
      {/* form fields */}
      <button type="submit">Place Order</button>
    </form>
  )
}

The action is a BFF mutation layer — it coordinates multiple backend operations behind a single call from the UI.


Part 5: BFF Patterns in Practice

Pattern 1: Adapter / Data Transformation

Your downstream API returns data in a shape designed for storage, not for display. The BFF transforms it.

// Downstream service returns:
// { usr_id: "u_123", usr_nm: "chanh", usr_email: "c@dev", is_active_flg: 1 }
 
// BFF transforms to what the UI actually needs:
function adaptUser(raw: RawUser): UIUser {
  return {
    id: raw.usr_id,
    name: raw.usr_nm,
    email: raw.usr_email,
    isActive: raw.is_active_flg === 1,
  }
}

Pattern 2: Aggregation

Combine multiple service calls into one frontend-friendly response.

// Instead of the browser making 3 calls, the BFF makes them server-side
const dashboardData = await Promise.all([
  userService.getProfile(userId),
  orderService.getRecent(userId, 5),
  analyticsService.getSummary(userId),
])

Pattern 3: Caching at the BFF Layer

The BFF can cache expensive downstream calls so multiple clients (or repeated renders) don't hit your services.

// Next.js fetch caching — built into Server Components
const data = await fetch('https://api.internal/heavy-endpoint', {
  next: { revalidate: 60 }, // Cache for 60 seconds
})

Pattern 4: Authentication Gateway

The BFF handles session validation and injects identity into downstream calls, so your services don't each need to manage auth.

export async function GET(request: Request) {
  const session = await getSession(request)
  if (!session) return new Response('Unauthorized', { status: 401 })
 
  // Downstream services receive a trusted internal token
  const data = await internalService.getData({
    headers: { 'X-User-Id': session.userId },
  })
 
  return Response.json(data)
}

Part 6: BFF vs Other Patterns

BFF vs API Gateway

These are often confused but solve different problems:

API GatewayBFF
PurposeCross-cutting concerns (rate limiting, auth, routing)Client-specific data shaping
GranularityCoarse-grainedFine-grained per client
Data transformationMinimalCore responsibility
Typical ownerPlatform/infra teamFrontend team
CountOne (shared)One per client type

They're complementary: an API Gateway sits in front of your BFFs, handling routing, rate limiting, and TLS termination, while each BFF handles the application logic for its client.

BFF vs GraphQL

GraphQL also solves the "different clients need different data" problem, but differently:

BFFGraphQL
ApproachMultiple dedicated backendsSingle flexible API
Client controlServer defines response shapeClient specifies query
ComplexitySimpler per BFFSchema/resolver infrastructure
CachingEasier (standard HTTP)Harder (query-based)
Best forDifferent clients with very different needsClients that need ad-hoc queries

Next.js with Server Components leans toward the BFF model: the server decides what data to fetch and send, giving you control and simplicity.


Summary

The BFF pattern solves a real architectural problem: a single general-purpose API cannot optimally serve multiple frontends with different needs. By creating a dedicated backend layer per client, you get:

  • Tailored data shapes — each client gets exactly what it needs
  • Aggregation on the server — fewer round trips, leaner payloads
  • Decoupling — change one client's BFF without affecting others
  • Frontend team ownership — the team who knows the UI shapes the data

Next.js operationalizes the BFF pattern naturally:

  • Server Components → server-side data fetching and aggregation
  • Route Handlers → BFF API endpoints co-located with your frontend
  • Server Actions → BFF mutation layer without a separate API

If you're building a full-stack Next.js app, you're already using the BFF pattern — understanding it by name helps you reason about where logic belongs and how to structure your application as it grows.

Further Reading

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.