← [ BACK_TO_NOTES ]

Next.js Route Handlers

Next.js App Router 用 route.ts 把文件系统路由暴露为标准 HTTP Endpoint,基于 Request/Response 处理方法、参数、输入校验、鉴权授权、错误映射与流式响应。

#concept #growing #frame #react

[!info] related notes

Next.js Route Handlers

一句话定义

Next.js Route Handler 是 App Router 的 HTTP transport boundary:用 route.ts 把一个 route segment 暴露成可被浏览器、移动端、第三方服务、Webhook、脚本等 HTTP Client 调用的 Endpoint。

最短对比:

page.tsx
→ URL → React UI / RSC

route.ts
→ URL → HTTP Response

它并不是 React 独有的后端模型,本质仍然是传统 Web 后端中的 Handler / Controller。

文件系统如何映射 HTTP Endpoint

app/
└── api/
    └── goals/
        ├── route.ts
        └── [goalId]/
            └── route.ts

对应:

/api/goals
/api/goals/:goalId

例如:

export async function GET() {
  return Response.json({ goals: [] })
}

同一个 route.ts 可以按 HTTP Method 导出:

export async function GET() {}
export async function POST() {}
export async function PATCH() {}
export async function DELETE() {}

这与 REST 风格的 Handler 没有本质区别:URL 表达资源位置,Method 表达操作语义。

Request / Response 是协议边界

Route Handler 优先使用 Web 标准:

Request
Response

例如:

export async function POST(request: Request) {
  const json = await request.json()

  return Response.json(
    { data: json },
    { status: 201 },
  )
}

也可以读取:

request.json()
request.formData()
request.headers
URL / search params

Next.js 还提供 NextRequest / NextResponse 作为带框架便利能力的扩展,但底层心智仍然是标准 HTTP Request / Response。

Dynamic Route Params

动态 API:

app/api/goals/[goalId]/route.ts
→ /api/goals/123

Handler 可读取 route params,再把它当作外部不可信输入处理。

重要区别:

URL 中存在 goalId
≠
goalId 已验证
≠
当前用户有权访问这个 Goal

动态 URL 只是定位信息,不承担数据关系与权限证明。

Route Handler 的正确分层

Route Handler 最自然的位置是 transport adapter:

HTTP Request
    ↓
Route Handler
    ↓
parse / validate / authenticate
    ↓
Application / Service
    ↓
Repository / Owning Service
    ↓
DB / External System

与 Handler-Service-Repository 分层 完全同构:

Route Handler ≈ Handler / Controller
Service       ≈ use case / business logic
Repository    ≈ data access abstraction

因此不应该把复杂事务、状态机、权限规则、跨表业务流程全部堆进 route.ts。

Request Body 永远是不可信输入

错误:

const body = await request.json()
await db.goal.create({ data: body })

HTTP payload 可以被任意客户端构造,所以:

HTTP body
→ unknown / untrusted
→ runtime validation
→ trusted DTO
→ Service

例如:

const parsed = createGoalSchema.safeParse(
  await request.json(),
)

if (!parsed.success) {
  return Response.json(
    { error: "INVALID_INPUT" },
    { status: 400 },
  )
}

const goal = await goalService.create(parsed.data)

TypeScript 类型只约束编译时开发代码,不能让网络输入自动获得 runtime trust。

Authentication 不等于 Authorization

对于:

GET /api/assessments/123

只判断:

session exists

是不够的。

需要分两步:

Authentication
→ 你是谁?

Authorization
→ 你能否访问 assessment 123?

如果只验证登录、不验证 object ownership / permission,就可能形成 IDOR / BOLA。

因此:

URL ID
≠ authorization proof

更稳的 Service 调用形态是:

await assessmentService.getForUser({
  assessmentId,
  userId: session.user.id,
})

相关安全心智模型见 Web 认证、HTTP 安全与浏览器边界:心智模型。

Error 应映射成稳定 HTTP Contract

内部错误和公开 HTTP 错误不是同一层。

Domain / Application Error
        ↓
Route Handler
        ↓
HTTP status + stable error code

例如:

Validation failed → 400
Unauthenticated   → 401
Forbidden         → 403
Resource missing  → 404
Conflict          → 409
Unexpected error  → 500

不要把数据库错误、内部 host、stack trace、token 等 implementation details 直接返回客户端。

Server log
→ detailed internal evidence

HTTP response
→ stable public contract

Route Handler 可以返回什么

Route Handler 是 HTTP Handler,不是 JSON-only Handler。它可以返回:

JSON
text
HTML
XML / RSS
binary/file
Redirect
ReadableStream

因此可用于:

  • JSON API
  • RSS / llms.txt
  • 文件下载
  • Webhook receiver
  • 流式响应
  • BFF proxy

Streaming

可以返回 Web ReadableStream:

Route Handler
→ Response(stream)

这使它能够承载流式 HTTP 响应。但对于长期 SSE、AI Runtime、WebSocket 等场景,还必须同时评估部署平台的超时、连接生命周期和代理层限制;不能因为框架 API 支持 Stream 就默认所有 hosting 环境都适合长期连接。

Route Handler 与 Layout 的关系

route.ts 不参与 React Layout 树:

layout.tsx
→ React UI hierarchy

route.ts
→ HTTP routing primitive

API Response 不会被 layout.tsx 包裹,也不参与普通 React client navigation 的 UI composition。

page.tsx 与 route.ts 的路由职责

同一个 route segment 不应同时试图让同一 URL 既成为 Page,又成为 Route Handler。

典型组织:

app/goals/page.tsx
→ /goals

app/api/goals/route.ts
→ /api/goals

这样 UI URL 与 HTTP API URL 的职责清晰。

缓存边界

不要因为上一节学了 Cache Components 就假设所有 Handler 都应该缓存。

write / private / high-freshness endpoint
→ 通常不缓存响应语义

stable GET data
→ 可把可缓存的数据读取抽到 cached helper

缓存仍应遵守 Next.js Cache Components、Next.js cacheLife 和数据 ownership 约束。

最短记忆

route.ts
= HTTP boundary

负责:
parse
validate
authenticate
authorize
translate errors
call service
build response

不负责:
把全部领域逻辑塞进 Handler
把 URL ID 当权限证明
把内部异常直接暴露
创建于 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
关闭浮层 / 取消命令;输入框内清空文本