Đơn Giản Hóa Giao Tiếp API với Mô Hình BFF trong Next.js
Trong một dự án gần đây của tôi, frontend phải xử lý 5 API khác nhau, mỗi API có một cấu trúc phản hồi khác nhau. Vấn đề với nhiều backend đã trở thành một khó khăn phổ biến khi các ứng dụng web hiện đại thường dựa vào nhiều API, microservices và tích hợp bên thứ ba. Mặc dù kiến trúc này rất mạnh mẽ, nhưng nó có thể tạo ra sự phức tạp cho frontend, chẳng hạn như xử lý nhiều nguồn dữ liệu, quản lý xác thực và đối phó với các định dạng phản hồi không nhất quán.
Mô Hình Backend-for-Frontend (BFF) là gì?
Mô hình Backend-for-Frontend (BFF) là một phương pháp kiến trúc, trong đó một lớp backend được tạo ra đặc biệt cho một ứng dụng frontend (ví dụ: web, mobile, hoặc desktop).
Thay vì để frontend giao tiếp trực tiếp với nhiều microservices, APIs hoặc cơ sở dữ liệu, BFF hoạt động như một adapter:
- Tổ chức dữ liệu từ nhiều nguồn khác nhau.
- Định hình phản hồi để phù hợp với yêu cầu của frontend.
- Quản lý xác thực và ủy quyền.
Tóm lại, BFF cung cấp một bề mặt API đơn giản, tùy chỉnh, giúp phát triển frontend nhanh chóng và dễ dàng hơn.
Lợi ích của mô hình BFF
- Giảm phức tạp cho frontend: Bởi vì lớp BFF hoạt động như một proxy, giao diện chỉ cần tương tác với một API duy nhất.
- Tối ưu hóa dữ liệu: Chỉ trả về những dữ liệu cần thiết.
- Cải thiện bảo mật: Tokens và thông tin xác thực được xử lý trên máy chủ, không phải trên máy khách.
- Tính linh hoạt: Mỗi frontend (web, mobile, IoT) có thể có BFF riêng, tùy chỉnh theo nhu cầu.
- Tính nhất quán: BFF đảm bảo định dạng phản hồi thống nhất giữa các dịch vụ backend khác nhau.
Triển khai mô hình BFF với Next.js và NestJS
Kiến trúc
- Frontend (Next): Render giao diện và chỉ tương tác với lớp BFF.
- Lớp BFF với Next API Routes: Hoạt động như một proxy giữa frontend và backend.
- Backend (Nest): Cung cấp logic miền, kết nối đến cơ sở dữ liệu và xuất khẩu microservices.
Đối với backend, chúng tôi có thể có các dịch vụ khác nhau chạy dưới dạng nhiều instance của ứng dụng Nest, mỗi dịch vụ backend xử lý các miền kinh doanh khác nhau như xác thực, quản lý hàng hóa, thông báo, v.v.
Ví dụ về controller trong NestJS
typescript
// auth.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get('user/:id')
async findUserById(@Param('id') id: string) {
return this.authService.findUserById(id);
}
}
Ví dụ về service trong NestJS
typescript
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
async findUserById(id: string) {
// Gọi đến DB (giả lập)
return { id, name: 'Alice', email: 'alice@example.com' };
}
}
Backend chạy tại http://localhost:4000/auth/user/:id.
Frontend
Lớp API BFF trong Next.js
typescript
// api/profile.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query;
const response = await fetch(`http://localhost:4000/auth/users/${id}`);
const user = await response.json();
// Định nghĩa cấu trúc phản hồi độc lập với backend.
res.status(200).json({
id: user.id,
displayName: user.name,
contact: user.email
});
}
Gọi BFF trong một component
typescript
// pages/dashboard.tsx
import { useEffect, useState } from 'react';
export default function Dashboard() {
const [profile, setProfile] = useState<any>(null);
useEffect(() => {
async function loadProfile() {
const res = await fetch('/api/profile?id=123'); // API duy nhất
const data = await res.json();
setProfile(data);
}
loadProfile();
}, []);
if (!profile) return <p>Loading...</p>;
return (
<div>
<h1>Chào mừng bạn, {profile.displayName}</h1>
<p>Email: {profile.contact}</p>
</div>
);
}
Frontend giờ đây không còn bị phụ thuộc vào backend, chúng tôi có khả năng cải thiện lớp BFF cũng như kết nối đến các nguồn backend khác nhau để đáp ứng mọi yêu cầu UI của frontend.
Những điểm cần lưu ý
- Giảm sự phụ thuộc giữa Frontend và Backend
- Frontend chỉ giao tiếp với BFF, không phải với nhiều microservices trực tiếp.
- Nếu các API backend thay đổi, chỉ cần cập nhật lớp BFF — frontend vẫn không bị ảnh hưởng.
- Điều này giảm thiểu nguy cơ hỏng hóc cho frontend và làm cho việc phát triển backend an toàn hơn.
- Quản lý độ phức tạp của Frontend
- Thay vì mỗi component phải xử lý nhiều cuộc gọi API, BFF tổng hợp các phản hồi vào một endpoint duy nhất.
- Các nhà phát triển frontend có thể tập trung hoàn toàn vào logic UI/UX mà không cần lo lắng về việc tổ chức dữ liệu, xử lý token hay định hình payload.
- Kết quả là mã React/Next.js sạch hơn và dễ bảo trì hơn.
- Tăng tính linh hoạt giữa các nền tảng
- Mỗi frontend (web, mobile, IoT) có thể có BFF riêng, được tối ưu hóa cho các nhu cầu riêng.
- Ứng dụng di động có thể nhận dữ liệu nhẹ hơn, trong khi ứng dụng web nhận payload chi tiết hơn — tất cả từ các BFF riêng biệt trỏ đến cùng một backend.
- Các dịch vụ backend vẫn nhất quán, trong khi BFF điều chỉnh cho từng ngữ cảnh frontend.
- Cải thiện bảo mật và quản lý
- API keys, tokens và logic nhạy cảm được giữ trong lớp BFF, không bao giờ bị lộ cho client.
- Nơi tập trung để áp dụng các chính sách giới hạn tần suất, ghi log và caching.
Một số trường hợp sử dụng mô hình BFF
- Ứng dụng thương mại điện tử: Hãy tưởng tượng một trang sản phẩm cần lấy chi tiết, đánh giá, gợi ý và giá cả. Nếu không có BFF, frontend của bạn sẽ thực hiện bốn cuộc gọi API khác nhau và kết hợp mọi thứ lại. Với BFF, bạn chỉ cần hỏi một endpoint, và nó sẽ cung cấp chính xác những gì UI cần trong một lần.
- Bảng điều khiển SaaS: Bảng điều khiển nổi tiếng vì kéo dữ liệu từ khắp nơi, phân tích, thanh toán, hồ sơ người dùng, thông báo. Một BFF hoạt động như người quản lý trung gian thu thập tất cả thông tin, để frontend của bạn không cần phải chạy theo năm dịch vụ khác nhau mỗi khi một trang được tải.
- Ứng dụng đa kênh (web + mobile): Một ứng dụng web có thể muốn toàn bộ dữ liệu, trong khi phiên bản di động chỉ cần một payload nhẹ hơn để tối ưu hiệu suất. Với các BFF riêng biệt, bạn có thể định hình phản hồi khác nhau cho mỗi client mà không cần chạm vào các dịch vụ backend.
- Môi trường microservices: Nếu hệ thống của bạn được chia thành nhiều microservices như trong trường hợp của tôi, việc tiết lộ tất cả trực tiếp cho frontend có thể nhanh chóng trở nên lộn xộn. Một BFF ẩn đi sự phức tạp đó và cung cấp một API sạch cho UI.
- Tích hợp bên thứ ba: Bạn đã bao giờ phải xử lý các API như Stripe, Twilio hoặc Salesforce chưa? Tất cả đều đi kèm với các định dạng khác nhau và những đặc điểm xác thực riêng. Một BFF có thể chuẩn hóa những phản hồi này và xử lý quản lý token ở hậu trường, vì vậy đội ngũ frontend của bạn không cần phải lo lắng về điều đó.
Kết luận
Mô hình Backend-for-Frontend (BFF) cung cấp một cách thực tiễn để giảm thiểu phức tạp, cải thiện bảo mật và tách biệt các đội ngũ frontend khỏi những thay đổi của backend. Bằng cách giới thiệu một lớp adapter mỏng, frontend nhận chính xác dữ liệu cần thiết ở định dạng đúng, trong khi các dịch vụ backend vẫn sạch sẽ và độc lập.
Như đã thấy ở trên, Next.js xử lý việc render UI và tổ chức API, trong khi NestJS tập trung vào logic kinh doanh cốt lõi và khả năng mở rộng, cùng nhau đạt được sự cân bằng giữa năng suất lập trình viên và độ bền của hệ thống.
Nếu chúng ta muốn đẩy thiết lập này xa hơn, chúng ta có thể xem xét việc:
- Phát triển API theo hợp đồng với
ts-rest- Đảm bảo rằng cả frontend và backend chia sẻ một hợp đồng kiểu mạnh.
- Ngăn chặn việc không khớp giữa các payload API và cải thiện trải nghiệm của nhà phát triển.
- Ví dụ: Định nghĩa API của bạn trong
ts-restmột lần và tạo ra các kiểu client + server.
- Xác thực & ủy quyền với Cognito hoặc Auth0
- Chuyển giao quản lý người dùng, quy trình đăng nhập và xử lý token cho các nhà cung cấp danh tính đã được kiểm chứng.
- BFF xác thực và làm mới token một cách an toàn thay mặt cho frontend.
- Giữ thông tin nhạy cảm tránh xa client.
- Caching & Cải thiện hiệu suất
- Thêm caching (ví dụ: Redis, caching biên với Vercel) để giảm thiểu các lượt gọi API.
- Cải thiện hiệu suất frontend và khả năng mở rộng backend.
- Quan sát và Ghi log
- BFF là nơi lý tưởng để tập trung ghi log, giám sát và báo cáo lỗi cho các yêu cầu frontend.