Validation
Check incoming data with a simple, readable rule syntax.
Validator.validate(data, schema) checks an object against a schema of rules and returns either the data or a list of errors per field.
import { Validator } from 'riser-bob' const result = Validator.validate( { name: 'Zen', age: 17, email: 'zen@example.com' }, { name: ['required', 'string'], age: ['required', 'number', 'min:13'], email: ['required', 'email'], }) if (!result.valid) { console.log(result.errors) // { field: ['message', ...] }}The result is either { valid: true, data, errors: {} } or { valid: false, errors }. Pass a type to get a typed data: Validator.validate<CreateUserInput>(body, CreateUserRule).
Rules
| Rule | Description |
|---|---|
required | Value must be present and not empty. |
nullable | null is allowed. Cannot be combined with required. |
string / number / boolean | Type checks. Numbers must be finite. |
email | Must be a valid email address. |
confirmed | Must match the <field>Confirmation value. |
min:n / max:n | Numeric limit for numbers, length limit for strings. |
minLength:n / maxLength:n | String length limits. The limit must be an integer. |
- When a type rule fails, the remaining rules of that field are skipped, so you get one clear message instead of a pile of them.
- Fields that are not in the schema are left in
datauntouched. Pick the fields you want explicitly before saving, instead of passing the whole body to your database.
Error messages
| Rule | Message |
|---|---|
required | name is required |
string | name must be a string |
number | age must be a number |
boolean | isCompleted must be a boolean |
email | email must be a valid email |
confirmed | password confirmation does not match |
min:13 on a number | age must be at least 13 |
max:120 on a number | age may not be greater than 120 |
min:3 on a string | name must be at least 3 characters |
minLength:8 | password must be at least 8 characters |
maxLength:50 | name may not exceed 50 characters |
Keep rules in their own files
The starter project keeps rules in their own files, so handlers stay short. The starter's create.ts is empty on purpose. Fill it with the rules of your feature:
import type { ValidationSchema } from "riserbob"; export const CreateUserRule = { name: ["required", "string", "minLength:3", "maxLength:50"], email: ["required", "email", "maxLength:255"], password: ["required", "string", "minLength:8", "maxLength:40"], age: ["nullable", "number", "min:1", "max:120"],} satisfies ValidationSchema;An update rule usually drops required, so a client can send only the fields it wants to change:
import type { ValidationSchema } from "riser-bob"; export const UpdateUserRule = { name: ["string", "minLength:3", "maxLength:50"], email: ["email", "maxLength:255"], age: ["nullable", "number", "min:1", "max:120"],} satisfies ValidationSchema;Use it in a handler
readonly create: SpongHandler = async (spongReq, spongRes) => { const body = await spongReq.json<Record<string, unknown>>(); const result = Validator.validate(body, CreateUserRule); if (!result.valid) { return spongRes.SpongUnprocessableContent({ success: false, message: "Validation failed", errors: result.errors, }); } // result.data is safe to use from here on return spongRes.SpongCreated({ success: true, message: "User created" });};{ "success": false, "message": "Validation failed", "errors": { "name": ["name must be at least 3 characters"], "email": ["email is required"] }}Validating form data
Validation works on a plain object. For a multipart request, build the object from the form first:
const formData = await spongReq.formData();const bio = formData.get("bio"); const result = Validator.validate( { bio: typeof bio === "string" ? bio : "" }, UpdateProfileRule,);Invalid schemas
A schema mistake throws a TypeError as soon as validation runs, so it never goes unnoticed. This covers an unknown rule, a rule that needs a limit but has none, a limit that is not a number, and required combined with nullable. Passing something that is not a plain object as the data throws too.