S

Validation

Configure field validation using FieldBuilder methods, presets, ValidationRules, or raw Zod schemas.

Validation

Every field in @saastro/forms needs validation. There are three ways to set it up, from simplest to most flexible:

ApproachUse WhenExample
FieldBuilder methodsMost forms — clean, chainable, type-safe.required().email().minLength(5)
PresetsCommon patterns — one word, no config.preset('password-strong')
ValidationRules objectJSON configs, database-stored forms, visual builders{ required: true, format: 'email' }
Raw Zod schemaComplex 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

MethodWhat It DoesField Types
.required(msg?)Field must have a valueAll
.optional()Field can be emptyAll
.email(msg?)Must be a valid emailString fields
.url(msg?)Must be a valid URLString fields
.minLength(n, msg?)Minimum character countString fields
.maxLengthValidation(n, msg?)Maximum character countString fields
.regex(pattern, msg?)Must match regex patternString fields
.numberRange(min?, max?)Number must be within rangeNumber/slider
.itemCount(min?, max?)Min/max selected itemsCheckbox-group, switch-group
.mustBeTrue(msg?)Must be checkedCheckbox, 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:

PresetWhat It ValidatesExample Format
emailEmail addressuser@example.com
username3-20 chars, alphanumeric + underscorejohn_doe
password-simple8+ charactersmypassword
password-medium8+ chars, uppercase, lowercase, numberMyPass123
password-strong8+ chars, uppercase, lowercase, number, specialMyP@ss123
ssnUS Social Security Number123-45-6789

Contact:

PresetWhat It ValidatesExample Format
phone-usUS phone number555-123-4567
phone-internationalInternational phone+1-555-123-4567
postal-code-usUS ZIP code12345 or 12345-6789
postal-code-caCanadian postal codeA1B 2C3
credit-cardCredit card number1234-5678-9012-3456

Technical:

PresetWhat It ValidatesExample Format
urlWeb URLhttps://example.com
slugURL-safe slugmy-blog-post
hex-colorHex color code#FF5733
ipv4IPv4 address192.168.1.1
date-isoISO date string2026-02-19
time-24h24-hour time14:30
alpha-onlyLetters onlyHelloWorld
alphanumericLetters and numbers onlyabc123

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
PropertyTypeDescription
requiredbooleanField must have a value
requiredMessagestringCustom required error message
presetstringApply a named validation preset
String rules — for text, email, tel, textarea, select, radio, combobox, etc.
PropertyTypeDescription
minLength / minLengthMessagenumber / stringMinimum character count
maxLength / maxLengthMessagenumber / stringMaximum character count
pattern / patternMessagestring / stringRegex pattern (as string)
format / formatMessage'email' | 'url' | 'uuid' | 'cuid' | 'emoji' / stringBuilt-in format check
Number rules — for number, slider
PropertyTypeDescription
min / minMessagenumber / stringMinimum value
max / maxMessagenumber / stringMaximum value
integer / integerMessageboolean / stringMust be a whole number
positive / positiveMessageboolean / stringMust be positive
Array rules — for checkbox-group, switch-group, button-checkbox, button-card
PropertyTypeDescription
minItems / minItemsMessagenumber / stringMinimum selected items
maxItems / maxItemsMessagenumber / stringMaximum selected items
Boolean rules — for checkbox, switch
PropertyTypeDescription
mustBeTrue / mustBeTrueMessageboolean / stringMust be checked/true
Date rules — for date, daterange
PropertyTypeDescription
minDate / minDateMessagestring / stringEarliest allowed date (ISO string)
maxDate / maxDateMessagestring / stringLatest allowed date (ISO string)
mustBeFuture / mustBeFutureMessageboolean / stringDate must be in the future
mustBePast / mustBePastMessageboolean / stringDate 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 TypesBase Zod Type
text, email, tel, textarea, select, radio, combobox, etc.z.string()
number, sliderz.number()
checkbox-group, switch-group, button-checkbox, button-cardz.array(z.string())
checkbox, switchz.boolean()
date, daterangez.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';