当后端从单体演进到微服务,前端面临一个经典问题:一个页面需要从 3-5 个微服务聚合数据,直接从客户端调用会导致请求瀑布、数据过度获取、协议不一致。BFF(Backend for Frontend)层就是解决方案——Next.js 天然就是一个 BFF,它的 Server Component 和 Server Action 直接充当前端专属的服务编排层。
1.1 什么是 BFF
没有 BFF:
浏览器 → 用户服务(获取用户信息)
浏览器 → 项目服务(获取项目列表)
浏览器 → 计费服务(获取订阅状态)
浏览器 → 通知服务(获取未读数)
= 4 次请求,客户端聚合,慢
有 BFF:
浏览器 → Next.js BFF → 用户服务
→ 项目服务 } 并行调用
→ 计费服务 } 服务端聚合
→ 通知服务
= 1 次请求(浏览器到 BFF),BFF 内部并行 4 次
1.2 Next.js 为什么是天然 BFF
传统 BFF:
浏览器 → React SPA → BFF 服务器(Express/NestJS)→ 微服务
需要单独维护一个 BFF 服务
Next.js BFF:
浏览器 → Next.js(Server Component 直接调用微服务)→ 微服务
BFF 逻辑就写在 Server Component / Server Action 里,不需要额外服务
BFF 能力 Next.js 对应 数据聚合 Server Component + Promise.all 数据裁剪 只返回前端需要的字段 协议转换 Server Action 内部调 gRPC/REST/GraphQL 认证传递 middleware + cookies 透传 缓存 unstable_cache / ISR 错误处理 try-catch + 降级 UI
2.1 为什么用 tRPC
REST API 的问题:
前端:fetch('/api/projects') → 响应类型是 any
后端:app.get('/api/projects') → 没有类型约束
tRPC 的方案:
前端:trpc.project.list.useQuery() → 自动推导返回类型
后端:router.project.list() → 入参和返回类型一致
从后端到前端,全链路类型安全,零代码生成。
2.2 tRPC + Next.js App Router
// lib/trpc/init.ts
import { initTRPC, TRPCError } from '@trpc/server'
import superjson from 'superjson'
import { type Context } from './context'
const t = initTRPC. context < Context >(). create ({
transformer: superjson,
})
export const router = t.router
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure. use ( async ({ ctx , next }) => {
if ( ! ctx.userId) {
throw new TRPCError ({ code: 'UNAUTHORIZED' })
}
return next ({ ctx: { ... ctx, userId: ctx.userId } })
})
// lib/trpc/context.ts
import { auth } from '@clerk/nextjs/server'
export async function createContext () {
const { userId , orgId } = await auth ()
return { userId, tenantId: orgId }
}
export type Context = Awaited < ReturnType < typeof createContext>>
2.3 定义 Router
// lib/trpc/routers/project.ts
import { z } from 'zod'
import { router, protectedProcedure } from '../init'
import { db } from '@/lib/db'
import { projects } from '@/lib/db/schema'
export const projectRouter = router ({
list: protectedProcedure
. input (z. object ({
page: z. number (). min ( 1 ). default ( 1 ),
limit: z. number (). min ( 1 ). max ( 100 ). default ( 20 ),
}))
. query ( async ({ ctx , input }) => {
const data = await db
. select ()
. from (projects)
. where ( eq (projects.tenantId, ctx.tenantId))
. offset ((input.page - 1 ) * input.limit)
. limit (input.limit)
return data
}),
create: protectedProcedure
. input (z. object ({
name: z. string (). min ( 1 ). max ( 100 ),
description: z. string (). max ( 500 ). optional (),
}))
. mutation ( async ({ ctx , input }) => {
const [ project ] = await db
. insert (projects)
. values ({ ... input, tenantId: ctx.tenantId })
. returning ()
return project
}),
delete: protectedProcedure
. input (z. object ({ id: z. string (). uuid () }))
. mutation ( async ({ ctx , input }) => {
await db
. delete (projects)
. where ( and (
eq (projects.id, input.id),
eq (projects.tenantId, ctx.tenantId),
))
}),
})
// lib/trpc/routers/_app.ts
import { router } from '../init'
import { projectRouter } from './project'
import { userRouter } from './user'
import { billingRouter } from './billing'
export const appRouter = router ({
project: projectRouter,
user: userRouter,
billing: billingRouter,
})
export type AppRouter = typeof appRouter
2.4 Server-Side 调用(RSC)
// lib/trpc/server.ts
import 'server-only'
import { createTRPCContext } from './context'
import { appRouter } from './routers/_app'
import { createCallerFactory } from '@trpc/server'
const createCaller = createCallerFactory (appRouter)
export async function caller () {
const ctx = await createTRPCContext ()
return createCaller (ctx)
}
// app/(dashboard)/projects/page.tsx
import { caller } from '@/lib/trpc/server'
export default async function ProjectsPage () {
const trpc = await caller ()
const projects = await trpc.project. list ({ page: 1 , limit: 20 })
return < ProjectList projects ={ projects } />
}
2.5 Client-Side 调用
// lib/trpc/client.ts
'use client'
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './routers/_app'
export const trpc = createTRPCReact < AppRouter >()
// components/project-list-client.tsx
'use client'
import { trpc } from '@/lib/trpc/client'
export function ProjectListClient () {
const { data , isLoading } = trpc.project.list. useQuery ({ page: 1 })
if (isLoading) return < Skeleton />
return (
< ul >
{ data?. map ( p => < li key ={ p.id } > { p.name } </ li >) }
</ ul >
)
}
3.1 并行聚合
// app/(dashboard)/page.tsx — Dashboard 页面聚合多个服务
export default async function DashboardPage () {
const trpc = await caller ()
// 并行调用 4 个服务
const [ projects , stats , notifications , billing ] = await Promise . all ([
trpc.project. list ({ page: 1 , limit: 5 }),
trpc.analytics. overview (),
trpc.notification. unreadCount (),
trpc.billing. currentPlan (),
])
return (
< div className = "grid grid-cols-2 gap-4" >
< RecentProjects projects ={ projects } />
< StatsCard stats ={ stats } />
< NotificationBell count ={ notifications } />
< PlanBadge plan ={ billing.plan } />
</ div >
)
}
3.2 瀑布模式(依赖关系)
// 有些调用依赖前一个结果
export default async function ProjectDetailPage ({ params } : Props) {
const { id } = await params
const trpc = await caller ()
// 第一层:获取项目信息
const project = await trpc.project. getById ({ id })
// 第二层:基于项目信息并行获取
const [ members , activity , files ] = await Promise . all ([
trpc.member. listByProject ({ projectId: project.id }),
trpc.activity. recent ({ projectId: project.id }),
trpc.file. list ({ projectId: project.id }),
])
return < ProjectDetail project ={ project } members ={ members } activity ={ activity } files ={ files } />
}
3.3 数据裁剪
// 微服务返回完整的用户对象(20+ 字段)
// BFF 只返回前端需要的字段
export const dashboardRouter = router ({
overview: protectedProcedure. query ( async ({ ctx }) => {
const fullUser = await userService. getUser (ctx.userId)
const fullProjects = await projectService. list (ctx.tenantId)
// 裁剪:只返回前端需要的
return {
user: {
name: fullUser.name,
avatar: fullUser.avatarUrl,
role: fullUser.role,
},
projects: fullProjects. map ( p => ({
id: p.id,
name: p.name,
updatedAt: p.updatedAt,
})),
}
}),
})
4.1 服务调用封装
// lib/services/base.ts
interface ServiceCallOptions {
timeout ?: number
retries ?: number
fallback ?: () => any
}
async function serviceCall < T >(
fn : () => Promise < T >,
options : ServiceCallOptions = {}
) : Promise < T > {
const { timeout = 5000 , retries = 2 , fallback } = options
for ( let attempt = 0 ; attempt <= retries; attempt ++ ) {
try {
const controller = new AbortController ()
const timer = setTimeout (() => controller. abort (), timeout)
const result = await fn ()
clearTimeout (timer)
return result
} catch (error) {
if (attempt === retries) {
// 所有重试失败,使用降级方案
if (fallback) return fallback () as T
throw error
}
// 指数退避
await new Promise ( r => setTimeout (r, Math. pow ( 2 , attempt) * 100 ))
}
}
throw new Error ( 'Unreachable' )
}
4.2 降级策略
// app/(dashboard)/page.tsx
export default async function DashboardPage () {
const trpc = await caller ()
const [ projects , stats , notifications ] = await Promise . allSettled ([
trpc.project. list ({ page: 1 }),
trpc.analytics. overview (),
trpc.notification. unreadCount (),
])
return (
< div >
{ projects.status === 'fulfilled'
? < ProjectList projects ={ projects.value } />
: < ProjectListFallback /> // 降级:显示缓存数据或空状态
}
{ stats.status === 'fulfilled'
? < StatsCard stats ={ stats.value } />
: < StatsCardSkeleton /> // 降级:显示骨架屏
}
{ notifications.status === 'fulfilled'
? < NotificationBell count ={ notifications.value } />
: < NotificationBell count ={ 0 } /> // 降级:显示 0
}
</ div >
)
}
4.3 缓存作为降级
import { unstable_cache } from 'next/cache'
const getCachedStats = unstable_cache (
async ( tenantId : string ) => {
return analyticsService. overview (tenantId)
},
[ 'analytics-overview' ],
{ revalidate: 300 } // 5 分钟缓存
)
// 服务挂了 → 返回 5 分钟前的缓存数据
// 比显示错误好得多
5.1 REST 服务调用
// lib/services/payment.ts
class PaymentService {
private baseUrl = process.env. PAYMENT_SERVICE_URL !
async getSubscription ( tenantId : string ) {
const res = await fetch ( `${ this . baseUrl }/subscriptions/${ tenantId }` , {
headers: {
Authorization: `Bearer ${ process . env . INTERNAL_API_KEY }` ,
'Content-Type' : 'application/json' ,
},
next: { revalidate: 60 , tags: [ 'subscription' ] },
})
if ( ! res.ok) throw new Error ( `Payment service error: ${ res . status }` )
return res. json ()
}
}
export const paymentService = new PaymentService ()
5.2 gRPC 集成
// lib/services/grpc-client.ts
import { createChannel, createClient } from 'nice-grpc'
import { UserServiceDefinition } from './generated/user'
const channel = createChannel (process.env. USER_SERVICE_GRPC_URL ! )
const userClient = createClient (UserServiceDefinition, channel)
export async function getUser ( userId : string ) {
return userClient. getUser ({ userId })
}
export async function listUsers ( tenantId : string ) {
return userClient. listUsers ({ tenantId })
}
5.3 多协议统一
// lib/services/index.ts — 统一服务层
import { paymentService } from './payment' // REST
import { getUser } from './grpc-client' // gRPC
import { analyticsClient } from './analytics' // GraphQL
// tRPC router 统一编排不同协议的服务
export const dashboardRouter = router ({
overview: protectedProcedure. query ( async ({ ctx }) => {
const [ user , subscription , analytics ] = await Promise . all ([
getUser (ctx.userId), // gRPC
paymentService. getSubscription (ctx.tenantId), // REST
analyticsClient. query ({ tenantId: ctx.tenantId }), // GraphQL
])
return { user, subscription, analytics }
}),
})
6.1 什么时候引入 BFF
单体后端 → 不需要 BFF,直接查数据库
2-3 个微服务 → Server Component 直接调用
5+ 个微服务 → 需要 BFF 层统一编排
跨团队服务 → BFF 是必须的
6.2 tRPC vs REST API Routes
场景 推荐 前端内部调用 tRPC(类型安全) 第三方 Webhook REST API Route 对外公开 API REST + OpenAPI 移动端共用 API REST 或 GraphQL
Next.js 即 BFF :Server Component 和 Server Action 天然充当服务编排层
tRPC :全链路类型安全(后端 → 前端),零代码生成,RSC + Client 双模式调用
服务编排 :Promise.all 并行聚合、Promise.allSettled 容错、数据裁剪减少传输
熔断降级 :超时 + 重试 + 降级(缓存数据 / 骨架屏 / 默认值)
多协议 :REST / gRPC / GraphQL 在 tRPC router 中统一编排
架构决策 :5+ 微服务时引入 BFF,对内 tRPC、对外 REST