BFF 架构设计
后端接口"为我而设"—— 前端工程师亲手写的服务端层,解决聚合、裁剪、鉴权三大痛点
学习时长:约 10 小时 | 前置:阶段 03 / 04 | 产出:可运行的 Node BFF 聚合服务
一、BFF 是什么?为什么需要它?
BFF = Backend For Frontend(为前端而生的后端)。 它是一层介于「前端应用」和「后端微服务」之间的轻量服务,由前端团队维护,专门为前端页面提供"刚刚好"的接口。
没有 BFF 时的三个痛点
- 接口太碎:订单详情页要调 5 个后端接口(订单/商品/优惠/物流/评价),前端串行请求,页面慢且脆弱。
- 字段太多:后端返回 80 个字段,前端只用 15 个,流量浪费、联调扯皮。
- 鉴权复杂:每个接口都要带 token、处理错误码,前端到处写重复逻辑。
二、架构位置:一张图看懂
浏览器 App
(Web/小程序/RN)→
📡 BFF 层
(Node.js,前端团队维护)→
后端微服务
(订单/商品/用户…)
关键认知:BFF 不是中间层"中转站",而是适配层——每个端(Web / App / 小程序)可以有自己独立的 BFF,各自裁剪数据、组织格式,互不影响。
三、BFF 的四大职责
| 职责 | 说明 | 示例 |
| 聚合 | 一次页面请求,BFF 并行调多个后端接口合并返回 | 订单详情 = 订单 + 商品 + 物流 |
| 裁剪 | 只返回前端需要的字段,按页面组织数据结构 | 去掉敏感字段、扁平化嵌套 |
| 鉴权/会话 | 统一解析 token、刷新、注入用户身份 | SSO 单点登录透传 |
| 协议适配 | 把后端 RPC/内部协议转成前端友好的 REST/GraphQL | 内部接口 → JSON API |
四、REST BFF vs GraphQL BFF
| 维度 | REST BFF | GraphQL BFF |
| 定义接口 | 每个页面一个聚合接口(后端写) | 一个 /graphql 端点,前端声明要什么 |
| 前端自由度 | 低:加字段要后端改 | 高:查询里加字段即可 |
| 性能控制 | 强:后端完全可控 | 需防 N+1 与超深查询(加复杂度限制) |
| 学习成本 | 低 | 中(Schema/Resolver/缓存) |
| 适用 | 页面稳定、接口固定 | 多端差异大、需求变化快 |
选型建议:团队小、页面少 → REST BFF 够用;多端(Web+App+小程序)需求差异大、后台系统多 → GraphQL BFF 更划算。
五、Node BFF 落地:NestJS 聚合三个后端接口
场景
订单详情页需要:订单信息(订单服务)+ 商品信息(商品服务)+ 物流信息(物流服务)。前端只想要一个 GET /api/order-detail/:id。
代码实现
// order.controller.ts —— BFF 暴露给前端的聚合接口
@Controller('order-detail')
export class OrderDetailController {
constructor(private readonly agg: OrderAggregateService) {}
@Get(':id')
async get(@Param('id') id: string, @Req() req: Request) {
// 1. 并行调用三个后端服务(Promise.all)
const [order, goods, logistics] = await Promise.all([
this.agg.getOrder(id, req.token),
this.agg.getGoods(id, req.token),
this.agg.getLogistics(id, req.token),
]);
// 2. 裁剪:只留前端需要的字段,按页面结构组装
return {
orderNo: order.orderNo,
status: order.status,
amount: order.amount, // 去掉内部字段
goods: goods.map(g => ({ name: g.name, img: g.img, price: g.price })),
logistics: logistics.tracks.slice(0, 3), // 只取最近 3 条
};
}
}
// order-aggregate.service.ts —— 内部服务调用 + 统一鉴权
@Injectable()
export class OrderAggregateService {
constructor(private readonly http: HttpService) {}
private headers(token: string) {
return { Authorization: `Bearer ${token}` };
}
getOrder(id: string, token: string) {
return firstValueFrom(this.http.get(`http://order-svc/orders/${id}`, { headers: this.headers(token) }).pipe(map(r => r.data)));
}
// getGoods / getLogistics 同理…
}
要点:① Promise.all 并行,总耗时 = 最慢的服务而非三个之和;② token 在 BFF 透传,后端无需再单独鉴权;③ 聚合逻辑写在 service 层,controller 保持薄。
六、鉴权与安全边界(BFF 必答面试点)
- Token 透传:前端登录拿到 token,BFF 统一解析(JWT 校验),后续调用后端时携带内部凭证,前端永不接触内部服务地址。
- SSO 单点登录:BFF 做登录态校验,未登录重定向到统一登录中心,登录后回跳带 code 换 token。
- 安全边界:BFF 只暴露白名单路由;内部服务只在内网可访问(BFF 到内部走内网域名);对上游接口做超时与熔断(如 3s 超时 + 降级返回默认值)。
- 错误归一:后端 500/超时 → BFF 统一转成前端约定的
{ code, message } 格式,前端只处理一种错误结构。
七、案例解析:聚合 5 个服务的订单详情页
背景
电商 App 订单详情页原实现:前端依次调 5 个接口(串行),最慢情况 6.8s 才出全。后端团队改接口排期要两周,业务等不起。
方案(前端团队自建 BFF,3 天上线)
- NestJS 起一个 order-bff 服务,暴露
/api/order-detail/:id
- 内部并行调 5 个服务,加 2s 超时与局部降级(物流挂了返回空数组,不影响主信息)
- 前端改一个接口地址,其余逻辑不动
结果与复盘
- 接口耗时 6.8s → 1.2s(-82%);页面整体可交互时间 -58%
- 后续加字段(如"售后状态")只改 BFF 一行聚合,前端零改动
- 教训:BFF 也要有监控(耗时/错误率/依赖服务健康),否则会成为新的单点;另外别把 BFF 写成"大杂烩",聚合逻辑过多会重蹈巨石覆辙,按业务域拆 BFF 服务
八、坑清单(一线浓缩)
坑 1:BFF 变成"透传层" —— 只转发不聚合,没解决任何问题还多一层延迟。解法:每个 BFF 接口必须有真实聚合/裁剪价值。
坑 2:串行请求 —— 忘了 Promise.all,接口耗时线性叠加。解法:无依赖的后端调用全部并行。
坑 3:没有超时与降级 —— 一个依赖服务慢,整个 BFF 卡死,连锁拖垮页面。解法:超时 + 熔断 + 默认值降级。
坑 4:token 泄露内部信息 —— BFF 日志里打印了完整 token / 内部接口地址。解法:日志脱敏、内部域名不落日志。
坑 5:GraphQL N+1 —— Resolver 每层都查一次库,请求爆炸。解法:DataLoader 批量加载。
坑 6:无监控就上线 —— BFF 挂了前端全挂,还没有告警。解法:接入日志 + 指标 + 错误追踪,依赖服务健康探测。
九、实战项目:给"商品列表页"写 BFF
要求
- 用 NestJS(或 Express)建 order-bff 服务
- mock 三个后端接口(商品/库存/评价),延迟 200ms 模拟真实网络
- BFF 暴露
GET /api/list:并行聚合 + 只返回前端需要的字段
- 一个依赖服务返回 500 时,BFF 降级(该字段返回空)而不是整体报错
通关标准
- curl 一次请求拿到完整列表数据(证明聚合成功)
- 停掉其中一个 mock 服务,接口仍返回 200(证明降级生效)
- 能口头讲清:BFF 四大职责、为什么用 Promise.all、token 怎么透传
十、通关自检题
Q1:BFF 和普通后端 API 有什么区别?展开答案
普通后端 API 面向通用业务(一接口多用),BFF 面向具体前端页面(一页面一接口),由前端团队维护,做聚合、裁剪、鉴权适配。
Q2:BFF 聚合时为什么用 Promise.all 而不是顺序 await?展开答案
顺序 await 总耗时 = 各接口之和;Promise.all 并发,总耗时 = 最慢接口耗时,页面响应快数倍。
Q3:BFF 里 token 如何处理才安全?展开答案
前端 → BFF 用用户 token;BFF 校验后转内部凭证调后端;内部服务只在内网;日志脱敏;BFF 不暴露内部接口地址。
Q4:什么情况不该上 BFF?展开答案
后端接口已经按页面组织好、团队无人会 Node、只做一个纯展示站 —— 加了 BFF 反而是负担。
Q5:BFF 会变成新单点吗?怎么防?展开答案
会。防:多实例部署 + 负载均衡、熔断降级、依赖健康探测、按业务域拆分 BFF、接入监控告警。