Request Body Validation
Use Schema to Define Data Structures
Say goodbye to the dark ages of manually writing if (typeof req.body.name !== 'string'):
typescript
import { ObjectType, String, Int, Optional } from 'farrow-schema'
// Define Schema (as simple as writing TypeScript interfaces)
class CreateUserInput extends ObjectType {
name = String // Required string
email = String // Required string
age = Optional(Int) // Optional integer
}
// Use Schema to validate request body
app.post('/users', { body: CreateUserInput }).use((req) => {
// req.body is validated, type-safe!
const { name, email, age } = req.body
const user = { id: Date.now(), name, email, age }
return Response.status(201).json(user)
})Magic that happens automatically:
- ✅ Compile time: TypeScript infers
req.bodytype - ✅ Runtime: Automatically validates data format
- ✅ Validation failure: Automatically returns 400 error
Nested Objects and Arrays
typescript
import { ObjectType, String, Int, List, Optional } from 'farrow-schema'
// Address information
class Address extends ObjectType {
street = String
city = String
zipCode = String
}
// User information (including nested objects)
class User extends ObjectType {
name = String
email = String
address = Address // Nested object
tags = List(String) // String array
age = Optional(Int) // Optional field
}
app.post('/users', { body: User }).use((req) => {
// req.body type:
// {
// name: string
// email: string
// address: { street: string, city: string, zipCode: string }
// tags: string[]
// age?: number
// }
return Response.status(201).json(req.body)
})Custom Validation Errors
typescript
app.post('/users', {
body: User
}, {
// Custom response for validation failure
onSchemaError: (error, input, next) => {
return Response.status(400).json({
error: 'Data validation failed',
field: error.path?.join('.'), // Error field path
message: error.message, // Error message
received: error.value // User input value
})
}
}).use((req) => {
return Response.status(201).json(createUser(req.body))
})Practical example:
User sends incorrect data:
json
{
"name": "Alice",
"email": "not-an-email",
"address": {
"street": "Main St",
"city": "NY"
// zipCode missing!
}
}Automatically returns error:
json
{
"error": "Data validation failed",
"field": "body.address.zipCode",
"message": "zipCode is required",
"received": undefined
}Complex Validation Scenarios
Union Types
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 automatic type narrowing
if (paymentMethod.type === 'credit_card') {
console.log(paymentMethod.cardNumber) // ✅ Type safe
} else if (paymentMethod.type === 'paypal') {
console.log(paymentMethod.email) // ✅ Type safe
}
return Response.status(201).json({ orderId: 123 })
})Custom Validators
typescript
import { ValidatorType, Validator } from 'farrow-schema/validator'
// Email 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('Invalid email format')
}
return this.Ok(result.value)
}
}
// Use custom validator
class SignUpInput extends ObjectType {
username = String
email = EmailType // Auto validates email format
password = String
}
app.post('/signup', { body: SignUpInput }).use((req) => {
const { username, email, password } = req.body
// email is format-validated
return Response.status(201).json({ username, email })
})Parameterized Validators
typescript
// String length validator
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(`Length must be between ${min}-${max} characters`)
}
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 and content are length-validated
return Response.status(201).json(req.body)
})Validation Modes
Strict Mode (Default)
typescript
class UserInput extends ObjectType {
name = String
age = Number
}
// Strict mode: No type conversion
app.post('/users', { body: UserInput }).use((req) => {
// If passed { name: "Alice", age: "25" }
// Validation fails because age must be number type
return Response.json(req.body)
})Lenient Mode
typescript
// Lenient mode: Attempt type conversion
app.post('/users', {
body: UserInput,
strict: false // Enable lenient mode
}).use((req) => {
// If passed { name: "Alice", age: "25" }
// Automatically converts to { name: "Alice", age: 25 }
return Response.json(req.body)
})Type conversion rules (lenient mode):
"25"→25"true"→true"2024-01-01"→Date
Query Parameter Validation
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 (required)
// page?: number (optional)
// limit?: number (optional)
return Response.json({
query: q,
page,
limit,
results: []
})
})Headers Validation
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 is validated to exist
return Response.json({ success: true })
})Cookies Validation
typescript
const SessionCookie = Struct({
sessionId: String
})
app.get('/profile', { cookies: SessionCookie }).use((req) => {
const { sessionId } = req.cookies
// sessionId is validated to exist
return Response.json({ sessionId })
})Combined Validation
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
// Both are validated
const user = authenticateToken(token)
const created = createArticle(user, article)
return Response.status(201).json(created)
})Error Handling Best Practices
Unified Error Format
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
}]
}
}
// Global configuration
const app = Http({
onSchemaError: (error) => {
return Response.status(400).json(formatSchemaError(error))
}
})Custom Business Validation
typescript
app.post('/users', { body: CreateUserInput }).use(async (req) => {
const { email, username } = req.body
// Execute business validation after Schema validation passes
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)
})Practical Example
Complete User Registration
typescript
import { ObjectType, String, Optional } from 'farrow-schema'
import { ValidatorType, Validator, RegExp } from 'farrow-schema/validator'
// Email 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)
}
}
// Password validator
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)
}
}
// Registration input
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
// Check if user already exists
const existing = await db.users.findOne({
$or: [{ email }, { username }]
})
if (existing) {
return Response.status(409).json({
error: 'User already exists'
})
}
// Create user
const hashedPassword = await bcrypt.hash(password, 10)
const user = await db.users.create({
username,
email,
password: hashedPassword,
age,
createdAt: new Date()
})
// Generate 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
})
})