Data Validation
ElyOS uses Valibot for validating all server action inputs and client-side data. Valibot is a lightweight, TypeScript-first validation library that’s tree-shaking-friendly and provides excellent type inference.
Official documentation: valibot.dev
Installation
Section titled “Installation”Valibot is already part of ElyOS dependencies:
import * as v from 'valibot';Basic Schemas
Section titled “Basic Schemas”// Primitivesv.string()v.number()v.boolean()
// Modifiersv.optional(v.string()) // undefined is acceptedv.nullable(v.string()) // null is acceptedv.nullish(v.string()) // null and undefined are accepted
// Complex typesv.array(v.string())v.object({ name: v.string(), age: v.number() })v.union([v.string(), v.number()])v.picklist(['admin', 'user', 'guest'])Chaining Validators (v.pipe)
Section titled “Chaining Validators (v.pipe)”The v.pipe function allows chaining multiple validators:
const nameSchema = v.pipe( v.string(), v.minLength(2), v.maxLength(100), v.trim());
const emailSchema = v.pipe(v.string(), v.email());const positiveInt = v.pipe(v.number(), v.integer(), v.minValue(1));Type Inference
Section titled “Type Inference”Valibot automatically infers TypeScript types from schemas:
const userSchema = v.object({ name: v.string(), email: v.pipe(v.string(), v.email()), role: v.picklist(['admin', 'user'])});
type User = v.InferOutput<typeof userSchema>;// → { name: string; email: string; role: 'admin' | 'user' }Server-Side Usage (Server Actions)
Section titled “Server-Side Usage (Server Actions)”The command wrapper automatically validates input against the provided schema. If validation fails, the handler doesn’t run.
import { command } from '$app/server';import * as v from 'valibot';
const schema = v.object({ title: v.pipe(v.string(), v.minLength(1)), priority: v.picklist(['low', 'medium', 'high'])});
export const createTask = command(schema, async (input) => { // input type: v.InferOutput<typeof schema> // only validated data reaches here
const task = await db.insert(tasks).values({ title: input.title, priority: input.priority });
return { success: true, task };});See Server Actions for more details.
Client-Side Usage
Section titled “Client-Side Usage”For client-side validation, use the safeParse function:
import * as v from 'valibot';
const loginSchema = v.object({ email: v.pipe(v.string(), v.email('Invalid email address')), password: v.pipe(v.string(), v.minLength(8, 'At least 8 characters required'))});
function validateLogin(data: unknown) { const result = v.safeParse(loginSchema, data);
if (!result.success) { const errors = result.issues.map(i => i.message); return { valid: false, errors }; }
return { valid: true, data: result.output };}parse vs safeParse
Section titled “parse vs safeParse”| Function | On Validation Failure | Return Value |
|---|---|---|
v.parse(schema, data) | Throws exception | validated data |
v.safeParse(schema, data) | Doesn’t throw | { success, output, issues } |
On the client side, safeParse is generally recommended; on the server, the command wrapper handles validation automatically.
Custom Error Messages
Section titled “Custom Error Messages”You can provide custom error messages for every validator:
const schema = v.object({ name: v.pipe( v.string('Name must be text'), v.minLength(2, 'Name must be at least 2 characters'), v.maxLength(100, 'Name can be at most 100 characters') ), email: v.pipe( v.string(), v.email('Invalid email address format') ), age: v.pipe( v.number('Age must be a number'), v.integer('Age must be an integer'), v.minValue(18, 'Must be at least 18 years old') )});Common Validation Patterns
Section titled “Common Validation Patterns”Email Validation
Section titled “Email Validation”const emailSchema = v.pipe( v.string(), v.email('Invalid email address'), v.toLowerCase());Password Validation
Section titled “Password Validation”const passwordSchema = v.pipe( v.string(), v.minLength(8, 'At least 8 characters'), v.regex(/[A-Z]/, 'At least one uppercase letter required'), v.regex(/[a-z]/, 'At least one lowercase letter required'), v.regex(/[0-9]/, 'At least one number required'));URL Validation
Section titled “URL Validation”const urlSchema = v.pipe( v.string(), v.url('Invalid URL format'));Date Validation
Section titled “Date Validation”const dateSchema = v.pipe( v.string(), v.isoDate('Invalid date format (ISO 8601 required)'));Enum Validation
Section titled “Enum Validation”const roleSchema = v.picklist(['admin', 'user', 'guest'], 'Invalid role');Complex Schemas
Section titled “Complex Schemas”Nested Objects
Section titled “Nested Objects”const addressSchema = v.object({ street: v.string(), city: v.string(), zipCode: v.pipe(v.string(), v.regex(/^\d{4}$/))});
const userSchema = v.object({ name: v.string(), email: v.pipe(v.string(), v.email()), address: addressSchema});Array Validation
Section titled “Array Validation”const tagsSchema = v.pipe( v.array(v.string()), v.minLength(1, 'At least one tag required'), v.maxLength(10, 'Maximum 10 tags allowed'));
const postSchema = v.object({ title: v.string(), tags: tagsSchema});Union Types
Section titled “Union Types”const idSchema = v.union([ v.pipe(v.string(), v.uuid()), v.pipe(v.number(), v.integer(), v.minValue(1))]);Transformations
Section titled “Transformations”Valibot supports data transformation during validation:
const trimmedString = v.pipe( v.string(), v.trim(), v.minLength(1));
const normalizedEmail = v.pipe( v.string(), v.email(), v.toLowerCase());
const parsedNumber = v.pipe( v.string(), v.transform(s => parseInt(s, 10)), v.number(), v.minValue(0));Further Information
Section titled “Further Information”- Valibot Official Documentation
- Server Actions — detailed server action usage
- Environment Validation — environment variable validation with Varlock