前端转全栈 06:API 设计——REST 契约、zod 校验与统一错误处理

前端全栈tutorialapirestzod校验错误处理

系列目录:本文是「前端转全栈: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.