首页  |  学习总览  |  ← 返回专题总览 进阶专题 03 · 服务端架构

BFF 架构设计

后端接口"为我而设"—— 前端工程师亲手写的服务端层,解决聚合、裁剪、鉴权三大痛点
学习时长:约 10 小时 | 前置:阶段 03 / 04 | 产出:可运行的 Node BFF 聚合服务

一、BFF 是什么?为什么需要它?

BFF = Backend For Frontend(为前端而生的后端)。 它是一层介于「前端应用」和「后端微服务」之间的轻量服务,由前端团队维护,专门为前端页面提供"刚刚好"的接口。

没有 BFF 时的三个痛点

  1. 接口太碎:订单详情页要调 5 个后端接口(订单/商品/优惠/物流/评价),前端串行请求,页面慢且脆弱。
  2. 字段太多:后端返回 80 个字段,前端只用 15 个,流量浪费、联调扯皮。
  3. 鉴权复杂:每个接口都要带 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 BFFGraphQL 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 必答面试点)

七、案例解析:聚合 5 个服务的订单详情页

背景

电商 App 订单详情页原实现:前端依次调 5 个接口(串行),最慢情况 6.8s 才出全。后端团队改接口排期要两周,业务等不起。

方案(前端团队自建 BFF,3 天上线)

  1. NestJS 起一个 order-bff 服务,暴露 /api/order-detail/:id
  2. 内部并行调 5 个服务,加 2s 超时与局部降级(物流挂了返回空数组,不影响主信息)
  3. 前端改一个接口地址,其余逻辑不动

结果与复盘

八、坑清单(一线浓缩)

坑 1:BFF 变成"透传层" —— 只转发不聚合,没解决任何问题还多一层延迟。解法:每个 BFF 接口必须有真实聚合/裁剪价值。
坑 2:串行请求 —— 忘了 Promise.all,接口耗时线性叠加。解法:无依赖的后端调用全部并行。
坑 3:没有超时与降级 —— 一个依赖服务慢,整个 BFF 卡死,连锁拖垮页面。解法:超时 + 熔断 + 默认值降级。
坑 4:token 泄露内部信息 —— BFF 日志里打印了完整 token / 内部接口地址。解法:日志脱敏、内部域名不落日志。
坑 5:GraphQL N+1 —— Resolver 每层都查一次库,请求爆炸。解法:DataLoader 批量加载。
坑 6:无监控就上线 —— BFF 挂了前端全挂,还没有告警。解法:接入日志 + 指标 + 错误追踪,依赖服务健康探测。

九、实战项目:给"商品列表页"写 BFF

要求

  1. 用 NestJS(或 Express)建 order-bff 服务
  2. mock 三个后端接口(商品/库存/评价),延迟 200ms 模拟真实网络
  3. BFF 暴露 GET /api/list:并行聚合 + 只返回前端需要的字段
  4. 一个依赖服务返回 500 时,BFF 降级(该字段返回空)而不是整体报错

通关标准

十、通关自检题

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、接入监控告警。