Back to blog

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

architecturenextjsbackendweb-developmentsystem design
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 APIBFF
Sở hữu bởiBackend teamFrontend team (hoặc full-stack)
Cấu trúc dữ liệuChung, tái sử dụng đượcTùy chỉnh cho một client
Tổng hợp dữ liệuKhông có (client tự làm)Thực hiện phía server
Số clientNhiềuMột
Tốc độ thay đổiChậm (breaking changes)Nhanh (co-evolve với UI)
VersioningPhứ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ó:

  1. 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
  2. Route Handlers (/app/api/...) — API endpoint phía server đặt cùng vị trí với frontend
  3. 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.tsx

Route 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 GatewayBFF
Mục đíchCross-cutting concerns (rate limiting, auth, routing)Định hình dữ liệu theo từng client
Độ chi tiếtCoarse-grainedFine-grained theo từng client
Chuyển đổi dữ liệuTối thiểuTrách nhiệm cốt lõi
Chủ sở hữu thông thườngPlatform/infra teamFrontend team
Số lượngMộ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:

BFFGraphQL
Cách tiếp cậnNhiều backend riêng biệtMột API linh hoạt duy nhất
Kiểm soát clientServer định nghĩa cấu trúc responseClient chỉ định query
Độ phức tạpĐơn giản hơn mỗi BFFSchema/resolver infrastructure
CachingDễ hơn (HTTP chuẩn)Khó hơn (query-based)
Phù hợp nhấtCác client rất khác nhau về nhu cầuClient 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.