Validation
The validation module provides composable rule factories and avalidate() function for checking input data against declarative rule sets.
Quick start
import { validate, required, string, integer, min, max, email } from '@strav/http'
const { data, errors } = validate(input, {
name: [required(), string(), min(3), max(100)],
email: [required(), email()],
age: [integer(), min(18)],
})
if (errors) {
return ctx.json({ errors }, 422)
}
// data is typed and contains only declared fields
Rules
Every rule is a factory function returning aRule object:
interface Rule {
name: string
validate(value: unknown): string | null // error message or null
}
All rules pass on null/undefined — use required() to enforce presence.
Note: Error messages shown in the table below are the default English fallbacks. These can be customized via i18n configuration by providing translations for validation keys like validation.required, validation.string, etc.
Available rules
| Rule | Description | Error message |
|---|---|---|
required() |
Value must not be null, undefined, or empty string | This field is required |
string() |
Must be a string | Must be a string |
integer() |
Must be an integer | Must be an integer |
number() |
Must be a number (not NaN) | Must be a number |
boolean() |
Must be a boolean | Must be a boolean |
min(n) |
Numbers: >= n. Strings: length >= n |
Must be at least :min / Must be at least :min characters |
max(n) |
Numbers: <= n. Strings: length <= n |
Must be at most :max / Must be at most :max characters |
email() |
Must be a valid email address | Must be a valid email address |
url() |
Must be a valid URL | Must be a valid URL |
regex(pattern: RegExp) |
Must match a RegExp pattern | Invalid format |
enumOf(enum) |
Must be a value of the given TypeScript enum | Must be one of: :values |
oneOf(values) |
Must be one of the given values | Must be one of: :values |
array() |
Must be an array | Must be an array |
Examples
import { required, string, min, max, email, enumOf, oneOf, regex } from '@strav/http'
import { UserRole } from '../enums/user'
// Username: required string between 3 and 30 characters
const usernameRules = [required(), string(), min(3), max(30)]
// Role: must be a valid UserRole enum value
const roleRules = [required(), enumOf(UserRole)]
// Status: must be one of specific string values (no enum)
const statusRules = [required(), oneOf(['active', 'inactive', 'suspended'])]
// Phone: optional, but must match a pattern if provided
const phoneRules = [string(), regex(/^\+\d{10,15}$/)]
validate()
function validate(input: unknown, rules: RuleSet): ValidationResult
- Synchronous — no async overhead.
- Runs rules per field, stopping at the first error for each field.
- Strips unknown fields from
data— only declared fields are returned. - Omits
undefinedvalues — fields not present in the input are excluded fromdata, making partial updates safe (only submitted fields are included). - Returns
errors: nullwhen all rules pass.
Return type
interface ValidationResult> {
data: T // only declared fields
errors: Record | null // null when valid
}
Typed results
Use a generic to get typeddata:
interface CreateUserInput {
name: string
email: string
role: string
}
const { data, errors } = validate(body, {
name: [required(), string()],
email: [required(), email()],
role: [required(), oneOf(['admin', 'user'])],
})
if (!errors) {
data.name // typed as string
data.email // typed as string
}
Usage in controllers
The typical pattern in a controller action:
async store(ctx: Context) {
const body = await ctx.body()
const { data, errors } = validate(body, UserRules.store)
if (errors) return ctx.json({ errors }, 422)
const user = await service.create(data)
return ctx.json(user, 201)
}
Generated validators
Runningbun strav generate:api auto-generates validators from schema field definitions. Each validator has store (with required() on required fields) and update (without required()) rule sets:
// app/validators/user_validator.ts — Generated by Strav
import { required, enumOf, string } from '@strav/http'
import { UserRole } from '../enums/user'
export const UserRules: Record = {
store: {
username: [required(), string()],
role: [enumOf(UserRole)],
},
update: {
username: [string()],
role: [enumOf(UserRole)],
},
}
Enum fields use enumOf(Enum) referencing the generated TypeScript enum, so validators stay in sync when enum values change. Fields with inline enumValues (no custom type) still use oneOf([...]).
System-managed fields (id, timestamps, parent FK) are excluded from generated validators. Reference FK fields (e.g., authorPid from t.reference('user')) are included using their FK column name and validated with the referenced PK's type.
Writing custom rules
A rule is just an object withname and validate:
import type { Rule } from '@strav/http'
function slug(): Rule {
return {
name: 'slug',
validate(value) {
if (value === undefined || value === null) return null
if (typeof value !== 'string') return 'Must be a string'
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return 'Must be a valid slug'
return null
},
}
}
Follow the convention: return null for undefined/null (let required() handle presence), return an error string on failure.