前端转全栈 06:API 设计——REST 契约、zod 校验与统一错误处理
系列目录:本文是「前端转全栈:Next.js + Supabase 实战」系列的第 6 篇。接口不只是「能跑通」,更要「健壮、可维护、不被坏数据搞崩」。
第 4 篇你写出了接口,但那版有个隐患:只做了「参数不能为空」的粗校验。真实世界,客户端传来的任何数据都不可信。这一篇讲工程化的接口该长什么样。
一、REST 资源式命名
把「数据资源」当名词,用方法表达动作:
GET /api/posts # 列表
GET /api/posts/:slug # 详情
POST /api/posts # 新建(通常需权限)
PUT /api/posts/:slug # 整体更新
DELETE /api/posts/:slug # 删除
反例:/api/getPost、/api/createNewPost——把动作塞进路径,既冗余又不一致。名词用复数,动作交给方法。
二、zod:运行时校验,绝不信任客户端
TypeScript 的类型只在编译期有效,运行时(接口收到请求那一刻)没有任何类型保护。必须用 zod 在运行时校验:
import { z } from "zod"
const CommentSchema = z.object({
post_slug: z.string().min(1),
content: z.string().min(1).max(2000),
})
export async function POST(request: Request) {
const body = await request.json()
const parsed = CommentSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: "校验失败", issues: parsed.error.issues },
{ status: 400 }
)
}
// parsed.data 此时类型安全、且保证符合约束
const { post_slug, content } = parsed.data
// ...写入数据库
}
好处:
- 自动挡校验:长度、类型、必填全自动,不用手写一堆
if。 - 类型推导:
parsed.data自动有正确 TS 类型,前后端共用一份 schema。 - .failFast 防御:非法数据在入口就被挡掉,绝不会进数据库。
三、统一错误响应结构
散落的 return NextResponse.json({ error: "..." }) 让前端每处都要换着解析。统一约定:
{ "error": "参数缺失", "code": "INVALID_PARAM", "field": "content" }
封装一个helper:
// lib/api.ts
import { NextResponse } from "next/server"
export function ApiError(message: string, status = 400, code = "BAD_REQUEST") {
return NextResponse.json({ error: message, code }, { status })
}
// 使用
if (!parsed.success) return ApiError("校验失败", 400, "VALIDATION")
if (!user) return ApiError("未登录", 401, "UNAUTHENTICATED")
前端就能用统一的 error.code 做分支处理,而不是比对文案字符串。
四、错误边界:别让异常变成 500 裸奔
未捕获的异常会返回 500,但响应体可能是技术栈跟踪,既难看又可能泄露内部信息。兜底:
export async function POST(request: Request) {
try {
// ...业务逻辑
} catch (err) {
console.error(err) // 服务端日志保留细节
return ApiError("服务器内部错误", 500, "INTERNAL")
}
}
安全原则:日志里可以记细节,返回给客户端的只能是最少信息。把堆栈回给用户 = 给黑客递刀。
五、前端视角的接口契约
既然你前后端都写,把 zod schema 抽成共享文件(shared/schema.ts),前端用它做表单校验,后端用它做接口校验——一份定义,两端共用,永不脱节。这正是全栈一体化的红利。
// 前端表单也用它
const result = CommentSchema.safeParse(formState)
if (!result.success) setErrors(result.error.flatten().fieldErrors)
总结
- REST:名词用复数,动作交给方法(GET/POST/PUT/DELETE)。
- 运行时校验用 zod,TS 类型管不了运行时的请求体。
- 统一错误结构
{ error, code },前端按code分支。 - 异常兜底,日志记细节、对外只给最少信息。
- 把 schema 抽成前后端共享,是全栈的「契约即代码」。
下一篇,我们补上全栈最不能少的一环——认证与授权:Supabase Auth / GitHub OAuth / RLS。
练习:把第 4 篇你写的
/api/hello改造成/api/comments风格的健壮接口:引入 zod 校验name字段(1-20 字符),用统一ApiError返回,并加try/catch兜底。
Comments
Sign in to leave a comment.