Skip to content

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:

  1. ✅ Compile time: TypeScript infers req.body type
  2. ✅ Runtime: Automatically validates data format
  3. ✅ 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
  })
})

This is a third-party Farrow documentation site | Built with ❤️ and TypeScript