Next.js Route Handlers
Next.js App Router 用 route.ts 把文件系统路由暴露为标准 HTTP Endpoint,基于 Request/Response 处理方法、参数、输入校验、鉴权授权、错误映射与流式响应。
[!info] related notes
- 所属 MOC:
- 前置概念: nextjs-file-system-routing
- 并列概念:
- 易混淆概念:
- 关系笔记: nextjs-route-handler-vs-server-component、nextjs-route-handler-bff-and-webhooks
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 当权限证明
把内部异常直接暴露