请求体验证
使用 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)
})自动发生的魔法:
- ✅ 编译时: TypeScript 推导
req.body类型 - ✅ 运行时: 自动验证数据格式
- ✅ 验证失败: 自动返回 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
})
})