Validation
Every field in @saastro/forms needs validation. There are three ways to set it up, from simplest to most flexible:
| Approach | Use When | Example |
|---|---|---|
| FieldBuilder methods | Most forms — clean, chainable, type-safe | .required().email().minLength(5) |
| Presets | Common patterns — one word, no config | .preset('password-strong') |
| ValidationRules object | JSON configs, database-stored forms, visual builders | { required: true, format: 'email' } |
| Raw Zod schema | Complex custom logic — refinements, transforms, pipes | .schema(z.string().email()) |
Most developers only need the first two. The rest are there when you need them.
FieldBuilder Methods
The recommended approach. Chain validation methods on any field:
import { FormBuilder } from '@saastro/forms';
const config = FormBuilder.create('signup')
.addField('name', (f) =>
f.type('text').label('Name').required().minLength(2, 'Name is too short'),
)
.addField('email', (f) =>
f.type('email').label('Email').required().email('Please enter a valid email'),
)
.addField('password', (f) =>
f
.type('text')
.label('Password')
.required()
.minLength(8)
.regex('^(?=.*[A-Z])(?=.*\\d)', 'Must have 1 uppercase and 1 number'),
)
.addField('terms', (f) =>
f.type('checkbox').label('I accept the terms').mustBeTrue('You must accept the terms'),
)
.addStep('main', ['name', 'email', 'password', 'terms'])
.build();
All Methods
| Method | What It Does | Field Types |
|---|---|---|
.required(msg?) | Field must have a value | All |
.optional() | Field can be empty | All |
.email(msg?) | Must be a valid email | String fields |
.url(msg?) | Must be a valid URL | String fields |
.minLength(n, msg?) | Minimum character count | String fields |
.maxLengthValidation(n, msg?) | Maximum character count | String fields |
.regex(pattern, msg?) | Must match regex pattern | String fields |
.numberRange(min?, max?) | Number must be within range | Number/slider |
.itemCount(min?, max?) | Min/max selected items | Checkbox-group, switch-group |
.mustBeTrue(msg?) | Must be checked | Checkbox, switch |
.preset(id) | Apply a named preset (see below) | All |
Every method accepts an optional custom error message. Without one, a sensible default is used.
Presets
For common validation patterns, use a single preset instead of multiple rules:
.addField('phone', (f) =>
f.type('tel').label('Phone').preset('phone-us')
)
.addField('password', (f) =>
f.type('text').label('Password').preset('password-strong')
)
Built-in Presets (19)
Identity & Auth:
| Preset | What It Validates | Example Format |
|---|---|---|
email | Email address | user@example.com |
username | 3-20 chars, alphanumeric + underscore | john_doe |
password-simple | 8+ characters | mypassword |
password-medium | 8+ chars, uppercase, lowercase, number | MyPass123 |
password-strong | 8+ chars, uppercase, lowercase, number, special | MyP@ss123 |
ssn | US Social Security Number | 123-45-6789 |
Contact:
| Preset | What It Validates | Example Format |
|---|---|---|
phone-us | US phone number | 555-123-4567 |
phone-international | International phone | +1-555-123-4567 |
postal-code-us | US ZIP code | 12345 or 12345-6789 |
postal-code-ca | Canadian postal code | A1B 2C3 |
credit-card | Credit card number | 1234-5678-9012-3456 |
Technical:
| Preset | What It Validates | Example Format |
|---|---|---|
url | Web URL | https://example.com |
slug | URL-safe slug | my-blog-post |
hex-color | Hex color code | #FF5733 |
ipv4 | IPv4 address | 192.168.1.1 |
date-iso | ISO date string | 2026-02-19 |
time-24h | 24-hour time | 14:30 |
alpha-only | Letters only | HelloWorld |
alphanumeric | Letters and numbers only | abc123 |
Custom Presets
Register your own:
import { registerPreset, getAvailablePresets } from '@saastro/forms';
registerPreset({
id: 'company-email',
label: 'Company Email',
description: 'Must be a @company.com email',
category: 'string',
rules: {
format: 'email',
pattern: '@company\\.com$',
patternMessage: 'Must be a @company.com address',
},
});
// Now use it like any built-in preset
.addField('email', (f) => f.type('email').label('Work Email').preset('company-email'))
Advanced: ValidationRules Object
Under the hood, FieldBuilder methods produce a ValidationRules object — a flat, JSON-serializable structure. You can write this object directly when your form configs come from a database, API, or visual builder.
// These two are equivalent:
.addField('email', (f) => f.type('email').label('Email').required().email())
.addField('email', (f) => f.type('email').label('Email').schema({
required: true,
format: 'email',
}))
This is what makes @saastro/forms work with the visual Form Builder — the entire validation config is JSON-storable.
All Properties
General
| Property | Type | Description |
|---|---|---|
required | boolean | Field must have a value |
requiredMessage | string | Custom required error message |
preset | string | Apply a named validation preset |
String rules — for text, email, tel, textarea, select, radio, combobox, etc.
| Property | Type | Description |
|---|---|---|
minLength / minLengthMessage | number / string | Minimum character count |
maxLength / maxLengthMessage | number / string | Maximum character count |
pattern / patternMessage | string / string | Regex pattern (as string) |
format / formatMessage | 'email' | 'url' | 'uuid' | 'cuid' | 'emoji' / string | Built-in format check |
Number rules — for number, slider
| Property | Type | Description |
|---|---|---|
min / minMessage | number / string | Minimum value |
max / maxMessage | number / string | Maximum value |
integer / integerMessage | boolean / string | Must be a whole number |
positive / positiveMessage | boolean / string | Must be positive |
Array rules — for checkbox-group, switch-group, button-checkbox, button-card
| Property | Type | Description |
|---|---|---|
minItems / minItemsMessage | number / string | Minimum selected items |
maxItems / maxItemsMessage | number / string | Maximum selected items |
Boolean rules — for checkbox, switch
| Property | Type | Description |
|---|---|---|
mustBeTrue / mustBeTrueMessage | boolean / string | Must be checked/true |
Date rules — for date, daterange
| Property | Type | Description |
|---|---|---|
minDate / minDateMessage | string / string | Earliest allowed date (ISO string) |
maxDate / maxDateMessage | string / string | Latest allowed date (ISO string) |
mustBeFuture / mustBeFutureMessage | boolean / string | Date must be in the future |
mustBePast / mustBePastMessage | boolean / string | Date must be in the past |
Escape Hatch: Raw Zod Schemas
When you need validation logic that can’t be expressed as simple rules — async checks, cross-field validation, transforms, or Zod pipes — pass a Zod schema directly:
import { z } from 'zod';
.addField('email', (f) =>
f.type('email')
.label('Email')
.schema(z.string().email().endsWith('@company.com', 'Must be a company email'))
)
.addField('age', (f) =>
f.type('text')
.label('Age')
.schema(z.coerce.number().min(18, 'Must be 18 or older').max(120))
)
Note: Once you use
.schema(zodSchema), the FieldBuilder validation methods (.required(),.email(), etc.) have no effect — the Zod schema takes full control. Don’t mix them.
Custom Validators (via Plugins)
For async or server-side validation (like checking if an email is already taken), use plugin validators:
import { definePlugin, PluginManager, FormBuilder } from '@saastro/forms';
const myPlugin = definePlugin({
name: 'my-validators',
version: '1.0.0',
validators: {
uniqueEmail: async (value, context) => {
const res = await fetch(`/api/check-email?email=${value}`);
const { exists } = await res.json();
return exists ? 'Email already registered' : true;
},
},
});
const pm = new PluginManager();
pm.register(myPlugin);
const config = FormBuilder.create('signup')
.usePlugins(pm)
.addField(
'email',
(f) => f.type('email').label('Email').required().email().customValidators('uniqueEmail'), // Runs after the built-in checks pass
)
.addStep('main', ['email'])
.build();
Validators return true for valid or an error message string for invalid. See Plugins for full details.
How It Works Internally
When a field uses ValidationRules (not a raw Zod schema), the compiler picks a base Zod type from the field type, then layers on your rules:
| Field Types | Base Zod Type |
|---|---|
| text, email, tel, textarea, select, radio, combobox, etc. | z.string() |
| number, slider | z.number() |
| checkbox-group, switch-group, button-checkbox, button-card | z.array(z.string()) |
| checkbox, switch | z.boolean() |
| date, daterange | z.date() (with preprocessing) |
The compilation order is: resolve preset → merge explicit rules → apply type-specific constraints → handle required: false with .optional().
API Reference
import type { ValidationRules, SchemaType } from '@saastro/forms';
import {
compileValidationRules, // (rules, fieldType) => ZodSchema
isZodSchema, // type guard for Zod schemas
isValidationRules, // type guard for ValidationRules objects
resolvePreset, // (id) => ValidationRules | undefined
registerPreset, // register a custom preset
getAvailablePresets, // list all presets (built-in + custom)
} from '@saastro/forms';