← [ BACK_TO_NOTES ]

Server Component、Server Action 与 Route Handler 的边界

用“读取与渲染 / React 内部 Mutation / 显式 HTTP Contract”三条调用路径区分 Next.js 的 Server Component、Server Action 与 Route Handler,并说明它们如何共享 Service 而不混淆 transport boundary。

#synthesis #growing #architecture #react

[!info] related notes

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
创建于 2026/9/16 更新于 2026/9/16
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本