看到 "Node.js 中间层退潮" 上热榜的时候,我正在改 BFF 层的一个跨域 bug。缘,妙不可言 😂
背景
先交代一下,我在一个 20 人的创业团队做全栈,两年前入职第一件事就是搭 Node.js 中间层。
当时的理由很充分:
scss
前端:React SPA
中间层:Node.js (Express) ← 我搭的
后端:Java 微服务 × 6
老板说前端不能直接调 Java,原因有三:
- 接口格式不统一------有的返回
{code, data},有的返回{status, result} - 跨域问题------6 个微服务 6 个域名
- 前端需要做接口聚合------一个页面要调 3-4 个接口拼数据
听起来确实需要一个 BFF 对吧?
我当时也这么想的。
两年后的真实状态
来看看我的 BFF 层现在长什么样:
bash
src/
├── routes/ # 87 个路由文件
├── controllers/ # 87 个 controller(一一对应)
├── services/ # 92 个 service
├── middleware/ # 15 个中间件
├── utils/ # 各种 helper
└── config/ # 3 套环境配置
87 个路由。你没看错。
其中有多少是"有意义的聚合逻辑"?我数了一下:12 个。
剩下的 75 个在干嘛?
javascript
// 真实代码(脱敏过),占 BFF 总代码量的 85%
router.get('/api/users/:id', async (req, res) => {
try {
const result = await javaService.get(`/user-service/users/${req.params.id}`);
res.json({ code: 0, data: result.data });
} catch (err) {
res.json({ code: -1, message: err.message });
}
});
透传。纯透传。
85% 的代码就是把前端的请求原封不动转发给 Java,再把 Java 的响应原封不动还给前端。连参数校验都没做(Java 那边做了)。
我维护了两年的"中间层",本质上是个超大号的 nginx proxy_pass。
算一笔账
这才是让我下定决心的原因。
服务器成本
BFF 层跑了 3 台 2C4G 的云服务器(高可用嘛),每台 ¥280/月:
ini
BFF 服务器: ¥280 × 3 = ¥840/月
PM2 进程管理: 包含在内
Nginx + SSL: 包含在内
监控告警(Datadog): ¥200/月
──────────────────────
合计: ≈¥1,040/月
人力成本
这个才是大头:
markdown
我每周花在 BFF 上的时间: ≈10 小时
- 后端新增接口 → 我同步加 BFF 路由: 4h
- Bug 修复 & 维护: 3h
- 升级依赖 / 安全补丁: 2h
- 写测试(虽然覆盖率一直上不去): 1h
按我的时薪折算: ≈¥1,500/周 = ¥6,000/月
一个月 ¥7,000+,维护一个"高级转发器"。
而且这还不算隐性成本------每次后端改了接口,我得同步改 BFF。经常出现「后端改了字段名忘了告诉我,前端页面挂了半天才发现」的情况。
这周动手了
热榜那篇文章让我下定了决心。我列了个清单:
1. 75 个纯透传路由 → Nginx 反向代理直接搞定
nginx
# 原来 87 个 Node.js 路由文件 → 现在 15 行 nginx 配置
location /api/user-service/ {
proxy_pass http://java-user-service:8080/;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/order-service/ {
proxy_pass http://java-order-service:8081/;
proxy_set_header X-Real-IP $remote_addr;
}
# ...6 个微服务全部直连
是的就这么简单。CORS?Nginx 加几行 header。认证?Java 那边本来就有鉴权中间件。
砍掉 BFF 的第一刀,省了 85% 的代码量。
2. 12 个聚合接口 → 前端用 Promise.all 自己聚合
typescript
// 以前:BFF 聚合三个接口
// GET /api/dashboard → 内部调 user + order + statistics
// 现在:前端直接并发请求
const [user, orders, stats] = await Promise.all([
fetch('/api/user-service/me'),
fetch('/api/order-service/recent'),
fetch('/api/stat-service/dashboard'),
]);
有人会说"这不就是把复杂度推给前端了吗?"
对,但前端框架现在处理这个太轻松了。React Query / SWR + 几行代码,还自带缓存和错误重试。比我手写的 BFF 聚合逻辑健壮得多。
3. 接口格式不统一?写个前端拦截器
typescript
// axios 拦截器统一格式,10 行代码解决
api.interceptors.response.use((response) => {
const data = response.data;
// Java 服务 A: {code, data}
// Java 服务 B: {status, result}
return {
success: (data.code === 0 || data.status === 'ok'),
data: data.data || data.result,
message: data.message || data.error,
};
});
以前用 BFF 做的"格式统一",一个拦截器就搞定了。
重构结果
花了两天时间,结果:
makefile
代码变化:
删除: 87 个路由文件 + 92 个 service + 所有 BFF 代码
新增: 15 行 Nginx 配置 + 1 个前端拦截器 + 3 个 Promise.all 聚合
服务器变化:
关掉: 3 台 BFF 云服务器
省钱: ¥1,040/月
人力变化:
我每周在 BFF 上的时间: 10h → 0h
后端改接口不用通知我了: priceless 😂
部署流程:
之前: 前端 + BFF + 后端 = 3 个部署流水线
现在: 前端 + 后端 = 2 个部署流水线
但是!不是所有中间层都该删
说几个真正需要 Node.js 中间层的场景:
1. SSR / SSG --- Next.js、Nuxt 这类框架本身就是 Node.js 中间层,这个有真实价值
2. 复杂的 BFF 编排 --- 如果你的聚合逻辑涉及条件分支、错误回退、事务补偿,前端处理确实吃力
3. AI API 代理 --- 这个是我新发现的场景,说来有点讽刺
我删掉 BFF 之后一个月,团队开始往产品里加 AI 功能。结果发现又需要一个"中间层"了:
javascript
// 又特么需要代理了 😂
// 前端不能直接调 AI API(密钥暴露 + CORS)
router.post('/api/ai/chat', async (req, res) => {
const result = await openai.chat.completions.create({
model: 'gpt-4o',
messages: req.body.messages,
});
res.json(result);
});
不过这次我学聪明了------没有自己搭 Node 服务,而是用了 API 聚合平台做代理。类似 ofox.ai 这种,统一一个 endpoint 调不同的模型,前端只需要配一个 base_url 就行,连 Node 中间层都省了。
typescript
// 前端直接调(API Key 通过后端环境变量注入到 SSR 页面)
const client = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_AI_KEY,
baseURL: 'https://api.ofox.ai/v1',
dangerouslyAllowBrowser: true, // 仅限内部工具
});
当然如果是面向用户的产品,密钥肯定还是得走后端。但对内部工具来说,这种方案省了一整个服务。
给还在纠结的人几个建议
先审计你的中间层代码------数一下有多少路由是纯透传的。如果超过 60%,认真考虑砍掉。
别因为"万一以后需要"而保留------YAGNI 原则。我在 BFF 层留了两年的"预留扩展点",一个都没用上。
分步来------不用一口气全砍。我是先灰度切了 10 个低风险接口,观察一周没问题,再切剩下的。
计算真实成本------不只是服务器费用。你的时间才是最贵的。每周花在同步 BFF 路由上的时间,完全可以用来做更有价值的事。
考虑团队现状------如果你的后端接口真的很乱、没有统一规范,BFF 层可能还是必要的过渡方案。但目标应该是推动后端统一,而不是用 BFF 永远兜底。
最后
Node.js 中间层不是原罪。无脑加中间层才是。
两年前我搭 BFF 的时候确实解决了实际问题,但随着项目发展,它的维护成本已经远超收益。
热榜说的"从前端救星到成本噩梦",我的体会是:它确实当过救星,但别让它变成你的舒适区。
该摊牌的时候就摊牌吧 ✌️
全栈开发一枚,最近在折腾去中间层化。下一篇可能会写 Nginx 反向代理的完整配置方案,感兴趣的朋友关注一波 👀