BFF Pattern: Backend for Frontend Giải Thích

Bạn đã xây dựng một REST API gọn gàng cho sản phẩm của mình. Rồi team mobile nói endpoint dashboard trả về quá nhiều dữ liệu. Team web nói họ cần gộp ba API call riêng lẻ thành một. Còn team TV app lại nói toàn bộ cấu trúc dữ liệu sai hoàn toàn với use case của họ.
API "dùng chung cho mọi client" của bạn đang bắt đầu nứt vỡ dưới áp lực từ nhiều client — mỗi client có nhu cầu khác nhau.
Đây chính là bài toán mà pattern Backend for Frontend (BFF) được thiết kế để giải quyết.
Bạn Sẽ Học Được Gì
✅ BFF pattern là gì và bài toán nó giải quyết
✅ BFF khác với general-purpose API như thế nào
✅ Khi nào nên dùng BFF và khi nào không
✅ Next.js triển khai BFF pattern theo cách tự nhiên như thế nào
✅ Ví dụ thực tế với Server Components và Route Handlers
Phần 1: Bài Toán — Một API, Nhiều Client
Các ứng dụng hiện đại hiếm khi chỉ phục vụ một client. Bạn có thể có:
- Web app với dashboard phức tạp cần nhiều dữ liệu
- Mobile app trên mạng chậm cần payload tối giản
- Smart TV app với cấu trúc UI hoàn toàn khác
- Third-party integration tiêu thụ dữ liệu theo cách riêng
Những Điểm Đau
1. Over-fetching / Under-fetching
Mỗi client nhận cùng một cấu trúc dữ liệu, dù không cần thiết. Mobile app fetch một đối tượng user 50 trường nhưng chỉ render 5 trường. Web app cần dữ liệu từ ba endpoint và phải thực hiện ba round trip.
2. Tight Coupling
Khi bạn thay đổi API để đáp ứng web app, bạn có nguy cơ làm hỏng mobile app. Mọi client đều phụ thuộc vào cùng một contract.
3. Logic Tổng Hợp Lọt Vào Frontend
Web app phải gọi /users, /orders, và /notifications riêng lẻ, rồi lắp ghép kết quả bằng JavaScript trên client. Logic tổng hợp đó đáng lẽ phải nằm ở server.
4. Mismatch Về Hiệu Năng
Mobile client trên mạng 3G có ngưỡng chịu đựng latency rất khác so với web app được render server-side. Một thiết kế API không thể phục vụ tối ưu cả hai.
Phần 2: BFF Pattern
Backend for Frontend (BFF) là một architectural pattern trong đó bạn tạo ra một backend service riêng biệt cho mỗi frontend client. Mỗi BFF được sở hữu và định hình bởi team frontend mà nó phục vụ.
Thuật ngữ này được đặt ra bởi Sam Newman (tác giả của Building Microservices) và được phổ biến tại SoundCloud.
BFF Làm Gì
BFF nằm giữa frontend và các downstream service (microservices, databases, third-party APIs). Nó:
- Tổng hợp dữ liệu từ nhiều service thành một response duy nhất
- Chuyển đổi dữ liệu sang đúng cấu trúc frontend cần
- Lọc các trường client không cần (giảm kích thước payload)
- Điều phối các chuỗi service call phức tạp
- Áp dụng auth logic và rate limiting riêng cho từng client
BFF vs General API
| Vấn đề | General API | BFF |
|---|---|---|
| Sở hữu bởi | Backend team | Frontend team (hoặc full-stack) |
| Cấu trúc dữ liệu | Chung, tái sử dụng được | Tùy chỉnh cho một client |
| Tổng hợp dữ liệu | Không có (client tự làm) | Thực hiện phía server |
| Số client | Nhiều | Một |
| Tốc độ thay đổi | Chậm (breaking changes) | Nhanh (co-evolve với UI) |
| Versioning | Phức tạp | Đơn giản hơn (1:1 với frontend) |
Phần 3: Khi Nào Nên Dùng BFF
Phù Hợp
- Nhiều frontend với nhu cầu dữ liệu khác nhau đáng kể
- Microservices backend — client không nên tự điều phối nhiều service call
- Mobile app nơi kích thước payload và latency quan trọng
- Full-stack team sở hữu cả frontend lẫn BFF của nó
- Third-party APIs mà bạn muốn chuẩn hóa sau một interface ổn định
Không Phù Hợp
- Một client duy nhất — nếu bạn chỉ có một frontend, BFF thêm overhead mà không có lợi ích
- CRUD app đơn giản — nếu data model ánh xạ trực tiếp lên UI, không cần translation layer
- Team nhỏ — duy trì nhiều BFF tốn chi phí hạ tầng; chỉ dùng khi nó giải quyết bài toán thực
Quy tắc ngón tay cái: Bắt đầu với một API duy nhất. Giới thiệu BFF khi bạn nhận thấy frontend team tốn nhiều công sức để tổng hợp hoặc chuyển đổi dữ liệu mà đáng lẽ có thể làm một lần trên server.
Phần 4: Next.js Như Một BFF Tự Nhiên
Đây là phần thực tế. Next.js về mặt kiến trúc chính là một BFF.
Khi bạn xây dựng ứng dụng Next.js, bạn có:
- React Server Components — component chạy trên server, fetch dữ liệu trực tiếp, và chỉ gửi HTML/data mà client cần
- Route Handlers (
/app/api/...) — API endpoint phía server đặt cùng vị trí với frontend - Server Actions — hàm server được gọi trực tiếp từ client component
Kết hợp lại, những tính năng này cho phép Next.js app của bạn hoạt động như một BFF layer giữa browser và các downstream data source.
Server Components Như BFF Aggregation
Trước React Server Components, việc fetch dữ liệu diễn ra trên browser. Browser gọi nhiều API và lắp ghép kết quả. Giờ đây logic đó chạy trên server.
Pattern cũ (client-side aggregation):
// ❌ Client fetch dữ liệu từ nhiều endpoint
'use client'
export default function Dashboard() {
const [user, setUser] = useState(null)
const [orders, setOrders] = useState([])
useEffect(() => {
// Ba round trip từ 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 với Server Components:
// ✅ Server Component fetch và tổng hợp — không có client round trip
import { getUser } from '@/lib/user'
import { getRecentOrders } from '@/lib/orders'
export default async function Dashboard() {
// Cả hai chạy trên server, song song
const [user, orders] = await Promise.all([
getUser(),
getRecentOrders({ limit: 5 }),
])
// Chỉ HTML được render gửi đến browser
return <DashboardView user={user} orders={orders} />
}Server component chính là BFF. Nó fetch từ database hoặc downstream API, tổng hợp, lọc, và chỉ gửi những gì UI cần.
Route Handlers Như BFF Endpoints
Với các nhu cầu dữ liệu do client điều khiển (search, mutations, real-time updates), Next.js Route Handlers cung cấp API endpoint theo phong cách BFF nằm ngay cạnh frontend code.
app/
├── api/
│ ├── user/
│ │ └── route.ts ← BFF endpoint cho dữ liệu user
│ ├── dashboard/
│ │ └── route.ts ← Tổng hợp user + orders + notifications
│ └── search/
│ └── route.ts ← Gọi search service, chuyển đổi kết quả
├── dashboard/
│ └── page.tsxRoute Handler hoạt động như một 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 })
}
// Tổng hợp từ nhiều nguồn song song
const [user, orders, notifications] = await Promise.all([
getUser(session.userId),
getOrders(session.userId, { limit: 5 }),
getNotifications(session.userId, { unreadOnly: true }),
])
// Định hình response cho đúng những gì dashboard cần
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,
})
}Mobile app có thể có một Route Handler hoàn toàn khác tại /api/mobile/dashboard trả về payload gọn hơn — một BFF riêng biệt cho một client riêng biệt.
Server Actions Như BFF Mutations
Server Actions là một chiều kích khác: chúng cho phép client component gọi logic phía server (mutations, form submissions) mà không cần endpoint API riêng.
// 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)
// Điều phối: tạo order + gửi 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}>
{/* các trường form */}
<button type="submit">Đặt Hàng</button>
</form>
)
}Action là một BFF mutation layer — nó điều phối nhiều backend operation sau một lần gọi duy nhất từ UI.
Phần 5: BFF Patterns Trong Thực Tế
Pattern 1: Adapter / Chuyển Đổi Dữ Liệu
Downstream API của bạn trả về dữ liệu theo cấu trúc thiết kế cho lưu trữ, không phải để hiển thị. BFF chuyển đổi nó.
// Downstream service trả về:
// { usr_id: "u_123", usr_nm: "chanh", usr_email: "c@dev", is_active_flg: 1 }
// BFF chuyển đổi sang những gì UI thực sự cần:
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: Tổng Hợp
Kết hợp nhiều service call thành một response thân thiện với frontend.
// Thay vì browser thực hiện 3 call, BFF làm chúng phía server
const dashboardData = await Promise.all([
userService.getProfile(userId),
orderService.getRecent(userId, 5),
analyticsService.getSummary(userId),
])Pattern 3: Caching Tại BFF Layer
BFF có thể cache các downstream call tốn kém để nhiều client (hoặc các render lặp lại) không phải hit vào service của bạn.
// Next.js fetch caching — tích hợp sẵn trong Server Components
const data = await fetch('https://api.internal/heavy-endpoint', {
next: { revalidate: 60 }, // Cache trong 60 giây
})Pattern 4: Authentication Gateway
BFF xử lý việc validate session và inject identity vào các downstream call, để các service của bạn không mỗi cái phải tự quản lý auth.
export async function GET(request: Request) {
const session = await getSession(request)
if (!session) return new Response('Unauthorized', { status: 401 })
// Downstream service nhận internal token đã được xác thực
const data = await internalService.getData({
headers: { 'X-User-Id': session.userId },
})
return Response.json(data)
}Phần 6: BFF vs Các Pattern Khác
BFF vs API Gateway
Hai pattern này thường bị nhầm lẫn nhưng giải quyết bài toán khác nhau:
| API Gateway | BFF | |
|---|---|---|
| Mục đích | Cross-cutting concerns (rate limiting, auth, routing) | Định hình dữ liệu theo từng client |
| Độ chi tiết | Coarse-grained | Fine-grained theo từng client |
| Chuyển đổi dữ liệu | Tối thiểu | Trách nhiệm cốt lõi |
| Chủ sở hữu thông thường | Platform/infra team | Frontend team |
| Số lượng | Một (dùng chung) | Một mỗi loại client |
Hai pattern bổ sung cho nhau: API Gateway nằm trước các BFF của bạn, xử lý routing, rate limiting và TLS termination, trong khi mỗi BFF xử lý application logic cho client của nó.
BFF vs GraphQL
GraphQL cũng giải quyết bài toán "các client khác nhau cần dữ liệu khác nhau", nhưng theo cách khác:
| BFF | GraphQL | |
|---|---|---|
| Cách tiếp cận | Nhiều backend riêng biệt | Một API linh hoạt duy nhất |
| Kiểm soát client | Server định nghĩa cấu trúc response | Client chỉ định query |
| Độ phức tạp | Đơn giản hơn mỗi BFF | Schema/resolver infrastructure |
| Caching | Dễ hơn (HTTP chuẩn) | Khó hơn (query-based) |
| Phù hợp nhất | Các client rất khác nhau về nhu cầu | Client cần ad-hoc queries |
Next.js với Server Components nghiêng về mô hình BFF: server quyết định dữ liệu nào cần fetch và gửi, mang lại cho bạn sự kiểm soát và đơn giản.
Tóm Tắt
BFF pattern giải quyết một bài toán kiến trúc thực sự: một general-purpose API duy nhất không thể phục vụ tối ưu nhiều frontend với các nhu cầu khác nhau. Bằng cách tạo ra một backend layer riêng biệt cho mỗi client, bạn có được:
- Cấu trúc dữ liệu tùy chỉnh — mỗi client nhận chính xác những gì cần
- Tổng hợp trên server — ít round trip hơn, payload gọn hơn
- Decoupling — thay đổi BFF của một client mà không ảnh hưởng client khác
- Quyền sở hữu của frontend team — team hiểu UI nhất sẽ định hình dữ liệu
Next.js triển khai BFF pattern một cách tự nhiên:
- Server Components → fetch và tổng hợp dữ liệu phía server
- Route Handlers → BFF API endpoint đặt cùng vị trí với frontend
- Server Actions → BFF mutation layer không cần API riêng biệt
Nếu bạn đang xây dựng full-stack Next.js app, bạn đã đang sử dụng BFF pattern — hiểu nó theo tên gọi giúp bạn lý luận rõ hơn về việc logic thuộc về đâu và cách cấu trúc ứng dụng khi nó phát triển.
Đọc Thêm
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.