Skip to content

请求体验证

使用 Schema 定义数据结构

告别手写 if (typeof req.body.name !== 'string') 的黑暗时代:

typescript
import { ObjectType, String, Int, Optional } from 'farrow-schema'

// 定义 Schema (像写 TypeScript 接口一样简单)
class CreateUserInput extends ObjectType {
  name = String           // 必填字符串
  email = String          // 必填字符串
  age = Optional(Int)     // 可选整数
}

// 使用 Schema 验证请求体
app.post('/users', { body: CreateUserInput }).use((req) => {
  // req.body 已经过验证,类型安全!
  const { name, email, age } = req.body
  const user = { id: Date.now(), name, email, age }

  return Response.status(201).json(user)
})

自动发生的魔法:

  1. ✅ 编译时: TypeScript 推导 req.body 类型
  2. ✅ 运行时: 自动验证数据格式
  3. ✅ 验证失败: 自动返回 400 错误

嵌套对象和数组

typescript
import { ObjectType, String, Int, List, Optional } from 'farrow-schema'

// 地址信息
class Address extends ObjectType {
  street = String
  city = String
  zipCode = String
}

// 用户信息 (包含嵌套对象)
class User extends ObjectType {
  name = String
  email = String
  address = Address           // 嵌套对象
  tags = List(String)         // 字符串数组
  age = Optional(Int)         // 可选字段
}

app.post('/users', { body: User }).use((req) => {
  // req.body 类型:
  // {
  //   name: string
  //   email: string
  //   address: { street: string, city: string, zipCode: string }
  //   tags: string[]
  //   age?: number
  // }

  return Response.status(201).json(req.body)
})

自定义验证错误

typescript
app.post('/users', {
  body: User
}, {
  // 自定义验证失败的响应
  onSchemaError: (error, input, next) => {
    return Response.status(400).json({
      error: '数据验证失败',
      field: error.path?.join('.'),    // 错误字段路径
      message: error.message,          // 错误消息
      received: error.value            // 用户传入的值
    })
  }
}).use((req) => {
  return Response.status(201).json(createUser(req.body))
})

实战示例:

用户传入错误数据:

json
{
  "name": "Alice",
  "email": "not-an-email",
  "address": {
    "street": "Main St",
    "city": "NY"
    // zipCode 缺失!
  }
}

自动返回错误:

json
{
  "error": "数据验证失败",
  "field": "body.address.zipCode",
  "message": "zipCode is required",
  "received": undefined
}

复杂验证场景

联合类型

typescript
import { Union, Literal, Struct } from 'farrow-schema'

const PaymentMethod = Union(
  Struct({
    type: Literal('credit_card'),
    cardNumber: String,
    cvv: String
  }),
  Struct({
    type: Literal('paypal'),
    email: String
  })
)

class CreateOrderInput extends ObjectType {
  items = List(String)
  paymentMethod = PaymentMethod
}

app.post('/orders', { body: CreateOrderInput }).use((req) => {
  const { items, paymentMethod } = req.body

  // TypeScript 自动类型窄化
  if (paymentMethod.type === 'credit_card') {
    console.log(paymentMethod.cardNumber)  // ✅ 类型安全
  } else if (paymentMethod.type === 'paypal') {
    console.log(paymentMethod.email)       // ✅ 类型安全
  }

  return Response.status(201).json({ orderId: 123 })
})

自定义验证器

typescript
import { ValidatorType, Validator } from 'farrow-schema/validator'

// 邮箱验证器
class EmailType extends ValidatorType<string> {
  validate(input: unknown) {
    const result = Validator.validate(String, input)
    if (result.kind === 'Err') return result

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!emailRegex.test(result.value)) {
      return this.Err('邮箱格式不正确')
    }

    return this.Ok(result.value)
  }
}

// 使用自定义验证器
class SignUpInput extends ObjectType {
  username = String
  email = EmailType  // 自动验证邮箱格式
  password = String
}

app.post('/signup', { body: SignUpInput }).use((req) => {
  const { username, email, password } = req.body
  // email 已经过格式验证
  return Response.status(201).json({ username, email })
})

参数化验证器

typescript
// 字符串长度验证器
const StringLength = (min: number, max: number) => {
  return class extends ValidatorType<string> {
    validate(input: unknown) {
      const result = Validator.validate(String, input)
      if (result.kind === 'Err') return result

      if (result.value.length < min || result.value.length > max) {
        return this.Err(`长度必须在 ${min}-${max} 个字符之间`)
      }

      return this.Ok(result.value)
    }
  }
}

class CreatePostInput extends ObjectType {
  title = StringLength(5, 100)
  content = StringLength(50, 5000)
}

app.post('/posts', { body: CreatePostInput }).use((req) => {
  // title 和 content 已验证长度
  return Response.status(201).json(req.body)
})

验证模式

严格模式 (默认)

typescript
class UserInput extends ObjectType {
  name = String
  age = Number
}

// 严格模式:不进行类型转换
app.post('/users', { body: UserInput }).use((req) => {
  // 如果传入 { name: "Alice", age: "25" }
  // 会验证失败,因为 age 必须是数字类型
  return Response.json(req.body)
})

宽松模式

typescript
// 宽松模式:尝试类型转换
app.post('/users', {
  body: UserInput,
  strict: false  // 启用宽松模式
}).use((req) => {
  // 如果传入 { name: "Alice", age: "25" }
  // 会自动转换为 { name: "Alice", age: 25 }
  return Response.json(req.body)
})

类型转换规则 (宽松模式):

  • "25"25
  • "true"true
  • "2024-01-01"Date

查询参数验证

typescript
import { Struct } from 'farrow-schema'

const SearchQuery = Struct({
  q: String,
  page: Optional(Int),
  limit: Optional(Int)
})

app.get('/search', { query: SearchQuery }).use((req) => {
  const { q, page = 1, limit = 10 } = req.query
  // q: string (必需)
  // page?: number (可选)
  // limit?: number (可选)

  return Response.json({
    query: q,
    page,
    limit,
    results: []
  })
})

Headers 验证

typescript
const RequiredHeaders = Struct({
  'authorization': String,
  'content-type': String
})

app.post('/api/protected', {
  headers: RequiredHeaders,
  body: SomeInput
}).use((req) => {
  const token = req.headers.authorization
  // token 已验证存在
  return Response.json({ success: true })
})

Cookies 验证

typescript
const SessionCookie = Struct({
  sessionId: String
})

app.get('/profile', { cookies: SessionCookie }).use((req) => {
  const { sessionId } = req.cookies
  // sessionId 已验证存在
  return Response.json({ sessionId })
})

组合验证

typescript
class CreateArticleInput extends ObjectType {
  title = StringLength(5, 100)
  content = StringLength(50, 5000)
  tags = List(String)
  status = Union(Literal('draft'), Literal('published'))
}

const AuthHeaders = Struct({
  authorization: String
})

app.post('/articles', {
  headers: AuthHeaders,
  body: CreateArticleInput
}).use((req) => {
  const token = req.headers.authorization
  const article = req.body

  // 两者都已验证
  const user = authenticateToken(token)
  const created = createArticle(user, article)

  return Response.status(201).json(created)
})

错误处理最佳实践

统一错误格式

typescript
interface ValidationError {
  error: string
  details: Array<{
    field: string
    message: string
  }>
}

function formatSchemaError(error: SchemaError): ValidationError {
  return {
    error: 'Validation failed',
    details: [{
      field: error.path?.join('.') || 'unknown',
      message: error.message
    }]
  }
}

// 全局配置
const app = Http({
  onSchemaError: (error) => {
    return Response.status(400).json(formatSchemaError(error))
  }
})

自定义业务验证

typescript
app.post('/users', { body: CreateUserInput }).use(async (req) => {
  const { email, username } = req.body

  // Schema 验证通过后,执行业务验证
  const existingUser = await db.users.findOne({
    $or: [{ email }, { username }]
  })

  if (existingUser) {
    return Response.status(409).json({
      error: 'User already exists',
      field: existingUser.email === email ? 'email' : 'username'
    })
  }

  const user = await createUser(req.body)
  return Response.status(201).json(user)
})

实战示例

完整的用户注册

typescript
import { ObjectType, String, Optional } from 'farrow-schema'
import { ValidatorType, Validator, RegExp } from 'farrow-schema/validator'

// 邮箱验证器
class EmailType extends ValidatorType<string> {
  validate(input: unknown) {
    const result = Validator.validate(String, input)
    if (result.kind === 'Err') return result
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(result.value)) {
      return this.Err('Invalid email format')
    }
    return this.Ok(result.value)
  }
}

// 密码验证器
const PasswordType = class extends ValidatorType<string> {
  validate(input: unknown) {
    const result = Validator.validate(String, input)
    if (result.kind === 'Err') return result
    if (result.value.length < 8) {
      return this.Err('Password must be at least 8 characters')
    }
    if (!/[A-Z]/.test(result.value)) {
      return this.Err('Password must contain uppercase letter')
    }
    if (!/[0-9]/.test(result.value)) {
      return this.Err('Password must contain number')
    }
    return this.Ok(result.value)
  }
}

// 注册输入
class SignUpInput extends ObjectType {
  username = RegExp(/^[a-zA-Z0-9_]{3,16}$/)
  email = EmailType
  password = PasswordType
  age = Optional(Int)
}

app.post('/auth/signup', {
  body: SignUpInput
}, {
  onSchemaError: (error) => {
    return Response.status(400).json({
      error: 'Validation failed',
      field: error.path?.join('.'),
      message: error.message
    })
  }
}).use(async (req) => {
  const { username, email, password, age } = req.body

  // 检查用户是否已存在
  const existing = await db.users.findOne({
    $or: [{ email }, { username }]
  })

  if (existing) {
    return Response.status(409).json({
      error: 'User already exists'
    })
  }

  // 创建用户
  const hashedPassword = await bcrypt.hash(password, 10)
  const user = await db.users.create({
    username,
    email,
    password: hashedPassword,
    age,
    createdAt: new Date()
  })

  // 生成 token
  const token = jwt.sign({ userId: user.id }, SECRET_KEY)

  return Response.status(201).json({
    user: {
      id: user.id,
      username: user.username,
      email: user.email
    },
    token
  })
})

这是一个第三方 Farrow 文档站 | 用 ❤️ 和 TypeScript 构建