The BFF Pattern in Next.js-The Architecture Decision
There is a moment in the life of most frontend developers when the API problem becomes obvious.
The BFF Pattern in Next.js-The Architecture Decision

There is a moment in the life of most frontend developers when the API problem becomes obvious.
You are building a feature. The backend gives you an endpoint. You call it. It returns 47 fields. You need 6. The other 41 travel across the network, get parsed by the browser, sit in memory, and do nothing.
Then the mobile team needs the same data. But they need it shaped differently. So the backend builds another endpoint. Or adds query parameters. Or you build a second fetch call and merge the responses on the frontend.
The codebase gets messier. The network gets chattier. The user experience gets slower.
There is a name for this problem. And there is a pattern that solves it.
It is called the Backend For Frontend — or BFF.
What BFF Actually Is
The Backend For Frontend pattern is an architectural approach where you create a dedicated backend layer — one that exists specifically to serve your frontend’s needs.
Not a general-purpose API. Not a microservice that every client shares. A backend that speaks your frontend’s language, returns exactly what your UI needs, and handles the complexity of talking to multiple backend services so your frontend does not have to.
The concept was first articulated clearly by Sam Newman in 2015 — but it has become dramatically more relevant in the Next.js era because Next.js gives you the perfect tool to implement it: API Routes and Server Actions.
Most Next.js developers use API routes for simple things — a contact form handler, a webhook receiver, a small utility endpoint. Very few use them as a proper BFF layer. That is the gap this article addresses.
The Problem BFF Solves — A Real Example
Let me show you a concrete situation before explaining the solution.
Imagine you are building a fintech dashboard. Your dashboard page needs to display:
- User account details (name, KYC status, account tier)
- Recent transactions (last 10, with amounts and status)
- Current profile
- Notifications (unread count)
Without BFF, your frontend makes four separate API calls:
// ❌ Without BFF — 4 separate calls from the browser
const [user, transactions, portfolio, notifications] = await Promise.all([
fetch('/api/users/me'),
fetch('/api/transactions?limit=10'),
fetch('/api/profile/summary'),
fetch('/api/notifications/unread-count')
]);
This works. But it has real problems:
Problem 1 — Waterfalling data: If any of these calls fail, your UI is in a partial state. You need to handle four separate loading states, four separate error states, and four separate retry mechanisms.
Problem 2 — Over-fetching: Each endpoint returns full objects. The user endpoint returns 30 fields. You display 4 of them. The transaction endpoint returns full transaction objects. You display amount, date, and status.
Problem 3 — Exposed backend structure: Your frontend now knows about four separate backend services. If the backend team restructures their services, your frontend breaks.
Problem 4 — Auth complexity duplicated everywhere: Every single call needs to attach the auth token, handle 401 responses, and manage token refresh. That logic lives in multiple places.
Now here is the same thing with a BFF:
// ✅ With BFF - 1 call from the browser
const dashboard = await fetch('/api/dashboard');
And the BFF layer handles everything else.
How BFF Works in Next.js
Next.js is uniquely positioned for BFF implementation because it runs on Node.js — meaning your API routes have full server capabilities. They can call external APIs, access environment variables securely, transform data, aggregate responses, and return exactly what your UI needs.
Here is the architecture:
Browser (React Components)
↓
Next.js API Routes (Your BFF Layer)
↓
External Backend Services / APIs
Your React components never talk to external APIs directly. They talk to your BFF. Your BFF talks to everyone else.
Building a Real BFF in Next.js — Step by Step
Let me build the dashboard example properly.
Project Structure
app/
├── api/
│ ├── dashboard/
│ │ └── route.ts ← BFF endpoint
│ ├── transactions/
│ │ └── route.ts
│ └── auth/
│ └── [...nextauth]/
│ └── route.ts
├── dashboard/
│ └── page.tsx ← Uses BFF
lib/
├── api-client.ts ← Internal helper for BFF to call backends
└── auth.ts
Step 1 — Create an Internal API Client
This is the module your BFF uses to call backend services. It handles auth, base URLs, and error handling in one place:
// lib/api-client.ts
const BACKEND_URL = process.env.BACKEND_API_URL;
interface ApiClientOptions {
token: string;
endpoint: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
}
export async function backendFetch<T>({
token,
endpoint,
method = 'GET',
body,
}: ApiClientOptions): Promise<T> {
const response = await fetch(`${BACKEND_URL}${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
// Next.js cache control
next: { revalidate: 30 }, // Cache for 30 seconds
});
if (!response.ok) {
throw new Error(`Backend error: ${response.status} ${endpoint}`);
}
return response.json();
}
Step 2 — Build the BFF Endpoint
This is the dashboard BFF route. It calls multiple backend services, shapes the data, and returns exactly what the UI needs — nothing more:
// app/api/dashboard/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { backendFetch } from '@/lib/api-client';
import { authOptions } from '@/lib/auth';
// Types for backend responses (usually much larger than this)
interface BackendUser {
id: string;
firstName: string;
lastName: string;
email: string;
kycStatus: string;
accountTier: string;
phoneNumber: string;
createdAt: string;
// ... 20 more fields we don't need
}
interface BackendTransaction {
id: string;
amount: number;
currency: string;
status: string;
type: string;
reference: string;
description: string;
createdAt: string;
// ... more fields
}
// Shaped types — exactly what the UI needs
interface DashboardResponse {
user: {
name: string;
kycStatus: string;
accountTier: string;
};
recentTransactions: {
id: string;
amount: number;
currency: string;
status: string;
date: string;
}[];
profileValue: number;
unreadNotifications: number;
}
export async function GET(request: NextRequest) {
try {
// 1. Get the session — auth is handled once here, not in every component
const session = await getServerSession(authOptions);
if (!session?.accessToken) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const token = session.accessToken as string;
// 2. Call all backend services in parallel
const [user, transactions, profile, notifications] =
await Promise.all([
backendFetch<BackendUser>({
token,
endpoint: '/users/me',
}),
backendFetch<{ data: BackendTransaction[] }>({
token,
endpoint: '/transactions?limit=10&sort=desc',
}),
backendFetch<{ totalValue: number }>({
token,
endpoint: '/profile/summary',
}),
backendFetch<{ count: number }>({
token,
endpoint: '/notifications/unread',
}),
]);
// 3. Shape the response — return only what the UI needs
const response: DashboardResponse = {
user: {
name: `${user.firstName} ${user.lastName}`,
kycStatus: user.kycStatus,
accountTier: user.accountTier,
},
recentTransactions: transactions.data.map((tx) => ({
id: tx.id,
amount: tx.amount,
currency: tx.currency,
status: tx.status,
date: tx.createdAt,
})),
profileValue: profile.totalValue,
unreadNotifications: notifications.count,
};
return NextResponse.json(response);
} catch (error) {
console.error('Dashboard error:', error);
return NextResponse.json(
{ error: 'Failed to load dashboard data' },
{ status: 500 }
);
}
}
Step 3 — Consume the BFF in Your Component
Now your React component is clean. No auth logic. No multiple fetches. No data transformation:
// app/dashboard/page.tsx
async function getDashboardData() {
const response = await fetch(
`${process.env.NEXTAUTH_URL}/api/dashboard`,
{ next: { revalidate: 30 } }
);
if (!response.ok) {
throw new Error('Failed to fetch dashboard');
}
return response.json();
}
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<main>
<h1>Welcome back, {data.user.name}</h1>
<p>Account tier: {data.user.accountTier}</p>
<p>Portfolio value: {data.portfolioValue}</p>
<p>Unread notifications: {data.unreadNotifications}</p>
<section>
<h2>Recent Transactions</h2>
{data.recentTransactions.map((tx) => (
<div key={tx.id}>
<span>{tx.amount} {tx.currency}</span>
<span>{tx.status}</span>
<span>{tx.date}</span>
</div>
))}
</section>
</main>
);
}
The component is now a pure presentation layer. It receives shaped data and renders it. Nothing else.
The Next.js 14+ Way — Server Actions as BFF
With Next.js App Router and Server Actions, you can take the BFF pattern even further. Instead of an API route, you can use a Server Action directly:
// app/dashboard/actions.ts
'use server'
import { getServerSession } from 'next-auth';
import { backendFetch } from '@/lib/api-client';
export async function getDashboardData() {
const session = await getServerSession();
if (!session?.accessToken) {
throw new Error('Unauthorized');
}
const [user, transactions, profile, notifications] =
await Promise.all([
backendFetch({ token: session.accessToken, endpoint: '/users/me' }),
backendFetch({ token: session.accessToken, endpoint: '/transactions?limit=10' }),
backendFetch({ token: session.accessToken, endpoint: '/profile/summary' }),
backendFetch({ token: session.accessToken, endpoint: '/notifications/unread' }),
]);
// Shape and return
return {
user: {
name: `${user.firstName} ${user.lastName}`,
kycStatus: user.kycStatus,
},
recentTransactions: transactions.data.slice(0, 10).map((tx) => ({
id: tx.id,
amount: tx.amount,
status: tx.status,
})),
portfolioValue: portfolio.totalValue,
unreadNotifications: notifications.count,
};
}
// app/dashboard/page.tsx
import { getDashboardData } from './actions';
export default async function DashboardPage() {
const data = await getDashboardData();
// Render...
}
Server Actions eliminate the HTTP round-trip between your component and the BFF entirely. The server function runs server-side directly. Faster. Cleaner. More secure.
What BFF Protects You From
Beyond performance and clean code, BFF gives you something less obvious but critically important — insulation.
When your backend team restructures their services — and they will — your frontend does not break. Your BFF absorbs the change. You update the BFF. The frontend component never knows anything changed.
This is especially valuable in fintech products where backend architecture evolves rapidly as the product scales. I’ve experienced this directly — backend changes that would have required frontend updates across multiple components instead required a single change to the BFF layer.
When NOT to Use BFF
BFF is not always the right answer. Be honest with yourself:
- Simple CRUD apps — if your frontend maps directly to your backend resources with no transformation needed, BFF adds complexity without benefit
- Small teams where you own the backend — if you control both frontend and backend, you can shape the API directly and skip the BFF layer
- Apps with a single client — BFF shines when multiple clients (web, mobile, internal tools) need the same backend data shaped differently. One client does not justify the pattern
Summary
The BFF pattern is not exotic architecture. In Next.js, it is a natural extension of API routes and Server Actions that most developers are already using — just not intentionally.
The key ideas:
- Your frontend should never talk directly to external backend services
- Your BFF aggregates, shapes, and secures data before it reaches the browser
- Auth is handled once in the BFF — not scattered across components
- Components become pure presentation layers — they receive shaped data and render it
- Next.js API routes and Server Actions are purpose-built for this pattern
Once you start thinking in BFF, you will find it difficult to go back. The separation of concerns is too clean. The components are too readable. The network is too quiet.
Originally published at https://timiebi.hashnode.dev on August 14, 2026.
메타데이터
- post_id
- 67ae3370d61d
- slug
- the-bff-pattern-in-next-js-the-architecture-decision-67ae3370d61d
- url
- https://medium.com/@kosutimiebinicholas/the-bff-pattern-in-next-js-the-architecture-decision-67ae3370d61d
- canonical_url
- https://medium.com/@kosutimiebinicholas/the-bff-pattern-in-next-js-the-architecture-decision-67ae3370d61d
- author_url
- https://medium.com/@kosutimiebinicholas
- status
- ok
- fetched_at
- 2026-08-16 02:00:23