Server Component、Server Action 与 Route Handler 的边界
用“读取与渲染 / React 内部 Mutation / 显式 HTTP Contract”三条调用路径区分 Next.js 的 Server Component、Server Action 与 Route Handler,并说明它们如何共享 Service 而不混淆 transport boundary。
[!info] related notes
- 所属 MOC:
- 相关概念: nextjs-server-component-data-fetching、nextjs-server-actions、nextjs-route-handlers、mutation-state-changing-operation
- 易混淆概念:
- 相关资源:
Server Component、Server Action 与 Route Handler 的边界
为什么要一起理解
Next.js 同一个应用里同时存在三种“都在服务器做事”的能力:
Server Component
Server Action
Route Handler
如果只看“它们都能访问数据库/API”,很容易写出重复 HTTP 层、滥用 Server Action 查询、把外部 API 做成 Action、或者把业务逻辑塞进入口层的问题。
真正应该区分的是:
1. 这次调用的主要意图是 Read/Render 还是 Mutation?
2. 调用者是 React UI 内部,还是一个显式 HTTP Client?
3. 真正的数据/业务 owner 在哪里?
三者最短模型
Server Component
→ server-side Read + Render
Server Action
→ React UI initiated Mutation
Route Handler
→ explicit HTTP Endpoint / Contract
这是三种不同的入口语义,不是三种“谁更高级”的后端写法。
Server Component:读取并生成 UI
典型:
export default async function GoalsPage() {
const user = await requireUser()
const goals = await goalService.listForUser(user.id)
return <GoalList goals={goals} />
}
调用链:
Request / navigation
→ Server Component
→ Service / Data Layer
→ DB or Owning Service API
→ RSC / UI
适合:
- 首屏读取
- server-rendered data
- 读取 Secret / DB / internal service
- 生成页面所需的 server data
如果同一个 Next.js 应用内部已经有 goalService.listForUser(),不要为了“统一 API”再绕:
Server Component
→ fetch('/api/goals')
→ Route Handler
→ goalService
应该直接共享 Service。
Server Action:React UI 内部 Mutation
典型:
React form / button
→ Server Action
→ validate/auth/authorize
→ Service command
→ truth changes
→ revalidate / redirect
例如:
"use server"
export async function createGoal(formData: FormData) {
const user = await requireUser()
const input = CreateGoalSchema.parse({
title: formData.get("title"),
})
await goalService.create({
actorId: user.id,
...input,
})
}
它最适合:
- 当前 React Web UI 自己的 create/update/delete
- form submit
- toggle / complete / archive
- login/logout 等服务端 Mutation
它不需要开发者设计稳定 /api/* URL,但仍然存在真实 Client→Server transport。
Route Handler:显式 HTTP Contract
典型:
Browser fetch
Mobile App
Desktop App
Stripe / GitHub / CMS
curl / external automation
↓
Route Handler
↓
Service
例如:
POST /api/goals
POST /api/webhooks/stripe
GET /api/notifications
适合:
- iOS / Android 客户端
- 第三方 API
- Webhook
- 浏览器需要普通 HTTP fetch 的接口
- BFF / protocol adapter
这里调用者知道并依赖的是:
URL + Method + Headers + Body + Response Contract
而不是某个 React function reference。
一张决策树
需要生成页面/读取初始数据?
│
├─ Yes
│ → Server Component
│
└─ No
↓
是 React UI 自己发起状态变更?
│
├─ Yes
│ → Server Action
│
└─ No
↓
需要稳定 HTTP Contract / 外部客户端?
│
└─ Yes
→ Route Handler
MemoFlow 示例
Web 首屏 Goal List
React page
→ Server Component
→ goalService.listForUser()
→ DB
Web 创建 Goal
Create button / form
→ Server Action
→ goalService.create()
→ DB
iOS 创建 Goal
iOS
→ POST /api/goals
→ Route Handler
→ goalService.create()
→ DB
所以同一个业务 Use Case:
goalService.create()
可以被不同 Adapter 调用:
Server Action ─┐
├→ goalService.create()
Route Handler ─┘
复用的是 Service,不是 transport。
BodySense 示例:Ownership 比 Next.js 入口更高优先级
如果 Assessment 由 Go Backend 拥有:
Read
Server Component
→ BodySense Go API
→ Go Service
→ DB
React UI Mutation
Server Action
→ BodySense Go API
→ Go Service
→ DB
Mobile / external client
Client
→ HTTP API / Route Handler / Go API
→ Owning Service
不能因为 Server Action 运行在服务器,就跳过 Go 直接 Prisma UPDATE。
所以入口选型之后仍然要继续问:
谁拥有这份数据和业务规则?
详见 数据所有权与服务边界。
三者都不应该成为 Domain Layer
无论是:
Server Component
Server Action
Route Handler
都不应该因为“能写服务器代码”就承载所有事务、状态机、领域规则。
更稳定的依赖:
UI / HTTP Adapter
→ Application Service
→ Domain / Policy
→ Repository / Downstream Service
入口层负责适配调用环境,Service 层负责业务 Use Case。
安全边界
Server Component
主要读取也仍需按照 user/session 做数据过滤和授权,不能因为代码在服务器就自动安全。
Server Action
Action arguments 来自客户端,应视为 untrusted:
validate
+ authenticate
+ authorize
UI 是否显示按钮不能代替 Action 内授权。
Route Handler
它是显式 public HTTP endpoint,同样:
authentication
≠ authorization
URL 中的资源 ID 也不是 ownership proof。
Mutation 的位置
这里的 Mutation 指 Mutation(状态变更操作):
create / update / delete / complete / cancel / publish ...
在普通 Next.js Web UI 中:
Read path
→ Server Component
Mutation path
→ Server Action
这只是一个很实用的默认分工,不是完整 CQRS,也不是绝对规则。
当 Mutation 需要提供给非 React 客户端时,仍然应该暴露 Route Handler 或独立服务 API。
常见误区
“既然 Action 最方便,所有后端调用都做 Action”
错误。外部系统和移动端需要稳定 HTTP Contract。
“既然 Route Handler 已经有了,Server Component 也统一 fetch API”
错误。同应用 server-side read 应优先直接共享 Service,避免无意义 HTTP hop。
“Server Action 是服务器函数,所以参数可信”
错误。调用源仍然是客户端。
“Server Action 可以直接改任何数据库”
错误。它不改变 Domain Ownership。
“Server Action = Service”
错误。Action 是 React mutation adapter;Service 才承载业务 Use Case。
最短记忆
Server Component
= Read / Render
Server Action
= React UI Mutation
Route Handler
= HTTP Contract
三者最终都应服从:
Service boundary + data ownership + authorization