Schemas
VeloxTS uses Zod for type-safe validation schemas. Schemas provide runtime validation while TypeScript infers static types automatically.
Basic Schema Types
Section titled “Basic Schema Types”String Validation
Section titled “String Validation”Zod provides comprehensive string validation with built-in formats and transformations.
import { z } from '@veloxts/velox';
const StringSchemas = z.object({ // Basic string validation name: z.string(), // Any string required: z.string().min(1), // Non-empty string
// Length constraints username: z.string().min(3).max(20), // 3-20 characters exactCode: z.string().length(6), // Exactly 6 characters
// Format validation email: z.string().email(), // Email format url: z.string().url(), // Valid URL uuid: z.string().uuid(), // UUID format datetime: z.string().datetime(), // ISO 8601 datetime
// Pattern matching alphanumeric: z.string().regex(/^[a-zA-Z0-9]+$/),
// Transformations trimmed: z.string().trim(), // Remove whitespace lowercase: z.string().toLowerCase(), // Convert to lowercase uppercase: z.string().toUpperCase(), // Convert to UPPERCASE});Number Validation
Section titled “Number Validation”Number schemas support range constraints, type coercion, and mathematical validations.
const NumberSchemas = z.object({ // Basic number types age: z.number().int().positive(), // Positive integer price: z.number().nonnegative(), // 0 or greater (allows decimals) discount: z.number().min(0).max(100), // Range: 0-100
// Mathematical constraints even: z.number().multipleOf(2), // Even numbers only percentage: z.number().min(0).max(1), // 0.0 to 1.0
// Negative numbers temperature: z.number().negative(), // Less than 0
// Optional with default quantity: z.number().int().default(1), // Defaults to 1 if undefined});Boolean and Literal Values
Section titled “Boolean and Literal Values”const BooleanSchemas = z.object({ // Boolean isActive: z.boolean(),
// Literal values (exact match required) status: z.literal('published'), // Only accepts "published" apiVersion: z.literal('v2'),
// Optional boolean with default newsletter: z.boolean().default(false),});const DateSchemas = z.object({ // Date objects birthDate: z.date(),
// Date with constraints adultBirthday: z.date().max(new Date(Date.now() - 18 * 365 * 24 * 60 * 60 * 1000)),
// ISO string dates (common for APIs) createdAt: z.string().datetime(), updatedAt: z.string().datetime().optional(),});Arrays and Objects
Section titled “Arrays and Objects”Array Validation
Section titled “Array Validation”Validate arrays with constraints on elements and length.
const ArraySchemas = z.object({ // Basic arrays tags: z.array(z.string()), // String array scores: z.array(z.number().int()), // Integer array
// Length constraints roles: z.array(z.string()).min(1), // At least 1 element topThree: z.array(z.string()).max(3), // At most 3 elements exactFive: z.array(z.number()).length(5), // Exactly 5 elements
// Non-empty arrays categories: z.array(z.string()).nonempty(), // Requires at least 1
// Default empty array interests: z.array(z.string()).default([]),});
// Practical example: Product with multiple imagesconst ProductSchema = z.object({ id: z.string().uuid(), name: z.string().min(1), images: z.array(z.object({ url: z.string().url(), alt: z.string(), isPrimary: z.boolean(), })).min(1), // At least one image required});Nested Objects
Section titled “Nested Objects”Build complex validation by nesting schemas.
const AddressSchema = z.object({ street: z.string().min(1), city: z.string().min(1), state: z.string().length(2), zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),});
const UserSchema = z.object({ id: z.string().uuid(), name: z.string().min(1), email: z.string().email(),
// Nested object address: AddressSchema,
// Optional nested object billingAddress: AddressSchema.optional(),
// Array of nested objects phoneNumbers: z.array(z.object({ type: z.enum(['home', 'work', 'mobile']), number: z.string().regex(/^\+?[1-9]\d{1,14}$/), })),});Optional vs Nullable
Section titled “Optional vs Nullable”Understanding the difference between optional and nullable is crucial.
// Optional - field can be undefined or omittedconst OptionalSchema = z.object({ description: z.string().optional(), // Valid: {} or { description: "text" } // Invalid: { description: null }});// Nullable - field can be null but must be presentconst NullableSchema = z.object({ description: z.string().nullable(), // Valid: { description: null } or { description: "text" } // Invalid: {} (field required)});// Optional + Nullable - can be undefined, null, or a stringconst FlexibleSchema = z.object({ description: z.string().nullable().optional(), // Valid: {}, { description: null }, { description: "text" }});
// Alternative syntax (same behavior)const AlternativeSchema = z.object({ description: z.string().nullish(), // Shorthand for nullable().optional()});Enums and Unions
Section titled “Enums and Unions”Restrict values to a predefined set.
// String enumconst UserRoleSchema = z.enum(['admin', 'editor', 'viewer']);type UserRole = z.infer<typeof UserRoleSchema>; // 'admin' | 'editor' | 'viewer'
// Native enum supportenum OrderStatus { Pending = 'PENDING', Processing = 'PROCESSING', Shipped = 'SHIPPED', Delivered = 'DELIVERED',}
const OrderSchema = z.object({ id: z.string().uuid(), status: z.nativeEnum(OrderStatus),});Unions
Section titled “Unions”Allow multiple types for a single field.
// Simple union - string or numberconst IdSchema = z.union([z.string(), z.number()]);
// Union with literalsconst StatusSchema = z.union([ z.literal('draft'), z.literal('published'), z.literal('archived'),]);
// Discriminated union - better type inferenceconst NotificationSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('email'), to: z.string().email(), subject: z.string(), body: z.string(), }), z.object({ type: z.literal('sms'), to: z.string(), message: z.string().max(160), }), z.object({ type: z.literal('push'), deviceId: z.string(), title: z.string(), body: z.string(), }),]);Custom Validation
Section titled “Custom Validation”Refine with Custom Rules
Section titled “Refine with Custom Rules”Add custom validation logic beyond built-in validators.
// Simple refine - single custom ruleconst PasswordSchema = z.string() .min(8, 'Password must be at least 8 characters') .refine( (password) => /[A-Z]/.test(password), { message: 'Password must contain at least one uppercase letter' } ) .refine( (password) => /[a-z]/.test(password), { message: 'Password must contain at least one lowercase letter' } ) .refine( (password) => /[0-9]/.test(password), { message: 'Password must contain at least one number' } );
// Object-level validationconst DateRangeSchema = z.object({ startDate: z.string().datetime(), endDate: z.string().datetime(),}).refine( (data) => new Date(data.endDate) > new Date(data.startDate), { message: 'End date must be after start date', path: ['endDate'] });
// Async validation (e.g., check database uniqueness)const UniqueEmailSchema = z.string().email().refine( async (email) => { const { db } = await import('@/api/database'); const existing = await db.user.findUnique({ where: { email } }); return !existing; }, { message: 'Email already in use' });SuperRefine for Complex Logic
Section titled “SuperRefine for Complex Logic”superRefine() provides fine-grained control over validation errors.
const RegistrationSchema = z.object({ email: z.string().email(), password: z.string().min(8), confirmPassword: z.string(), age: z.number().int(),}).superRefine((data, ctx) => { // Password confirmation check if (data.password !== data.confirmPassword) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Passwords do not match', path: ['confirmPassword'], }); }
// Age requirement for email domain if (data.email.endsWith('@school.edu') && data.age < 13) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Students must be at least 13 years old', path: ['age'], }); }});Schema Composition
Section titled “Schema Composition”Extend Schemas
Section titled “Extend Schemas”Build on existing schemas by adding fields.
const BaseUserSchema = z.object({ name: z.string().min(1), email: z.string().email(),});
// Add new fieldsconst UserWithIdSchema = BaseUserSchema.extend({ id: z.string().uuid(), createdAt: z.string().datetime(),});
// Extend and overrideconst AdminUserSchema = BaseUserSchema.extend({ email: z.string().email().endsWith('@admin.example.com'), // Override validation permissions: z.array(z.string()), // Add new field});Merge Schemas
Section titled “Merge Schemas”Combine multiple schemas into one.
const ContactInfoSchema = z.object({ email: z.string().email(), phone: z.string().optional(),});
const AddressInfoSchema = z.object({ street: z.string(), city: z.string(),});
// Merge two schemasconst FullContactSchema = ContactInfoSchema.merge(AddressInfoSchema);// Result: { email, phone?, street, city }Pick and Omit
Section titled “Pick and Omit”Extract or exclude specific fields.
const UserSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email(), password: z.string(), createdAt: z.string().datetime(),});
// Pick only specific fieldsconst UserPublicSchema = UserSchema.pick({ id: true, name: true, email: true });// Result: { id, name, email }
// Omit sensitive fieldsconst UserSafeSchema = UserSchema.omit({ password: true });// Result: { id, name, email, createdAt }Partial and Required
Section titled “Partial and Required”Make fields optional or required.
const CreateUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), age: z.number().int().positive(),});
// Make all fields optional (useful for updates)const UpdateUserSchema = CreateUserSchema.partial();// All fields become optional: { name?, email?, age? }
// Make specific fields optionalconst PartialNameSchema = CreateUserSchema.partial({ name: true });// Result: { name?, email, age }
// Make all fields required (opposite of partial)const OptionalSchema = z.object({ name: z.string().optional(), email: z.string().optional(),});const RequiredSchema = OptionalSchema.required();// All fields become required: { name, email }Custom Error Messages
Section titled “Custom Error Messages”Customize validation error messages for better UX.
// Field-level custom messagesconst UserSchema = z.object({ name: z.string({ required_error: 'Name is required', invalid_type_error: 'Name must be a string', }).min(1, 'Name cannot be empty'),
email: z.string() .min(1, 'Email is required') .email('Invalid email format'),
age: z.number({ required_error: 'Age is required', invalid_type_error: 'Age must be a number', }) .int('Age must be a whole number') .positive('Age must be positive') .min(18, 'Must be at least 18 years old'),});
// Validation with custom errorsconst PriceSchema = z.number() .min(0, { message: 'Price cannot be negative' }) .max(999999, { message: 'Price cannot exceed $999,999' }) .refine( (price) => price % 0.01 === 0, { message: 'Price must have at most 2 decimal places' } );Procedure Integration
Section titled “Procedure Integration”Schemas integrate seamlessly with VeloxTS procedures via .input() and .output().
Input Validation
Section titled “Input Validation”import { procedure, procedures } from '@veloxts/velox';import { z } from '@veloxts/velox';
// Define input schemasconst CreateProductInput = z.object({ name: z.string().min(1).max(200), description: z.string().max(2000), price: z.number().positive(), categoryId: z.string().uuid(), tags: z.array(z.string()).max(10).default([]),});
const UpdateProductInput = CreateProductInput.partial().extend({ id: z.string().uuid(),});
// Use in proceduresexport const productProcedures = procedures('products', { createProduct: procedure() .input(CreateProductInput) .output(ProductSchema) .mutation(async ({ input, ctx }) => { // input is fully typed and validated const product = await ctx.db.product.create({ data: { name: input.name, description: input.description, price: input.price, categoryId: input.categoryId, tags: input.tags, }, }); return product; }),
updateProduct: procedure() .input(UpdateProductInput) .output(ProductSchema) .mutation(async ({ input, ctx }) => { const { id, ...data } = input; return ctx.db.product.update({ where: { id }, data, }); }),});Output Validation
Section titled “Output Validation”Output schemas ensure your procedures return correctly shaped data.
// Define output schemaconst ProductSchema = z.object({ id: z.string().uuid(), name: z.string(), description: z.string(), price: z.number(), categoryId: z.string().uuid(), tags: z.array(z.string()), createdAt: z.string().datetime(), updatedAt: z.string().datetime(),});
// List with pagination metadataconst ProductListSchema = z.object({ data: z.array(ProductSchema), pagination: z.object({ page: z.number().int(), limit: z.number().int(), total: z.number().int(), totalPages: z.number().int(), }),});
export const productProcedures = procedures('products', { getProduct: procedure() .input(z.object({ id: z.string().uuid() })) .output(ProductSchema) // Validates single product .query(async ({ input, ctx }) => { const product = await ctx.db.product.findUnique({ where: { id: input.id } }); if (!product) { throw new Error('Product not found'); } return product; }),
listProducts: procedure() .input(z.object({ page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(100).default(20), })) .output(ProductListSchema) // Validates paginated response .query(async ({ input, ctx }) => { const [data, total] = await Promise.all([ ctx.db.product.findMany({ skip: (input.page - 1) * input.limit, take: input.limit, }), ctx.db.product.count(), ]);
return { data, pagination: { page: input.page, limit: input.limit, total, totalPages: Math.ceil(total / input.limit), }, }; }),});Type Inference
Section titled “Type Inference”Zod provides powerful type inference utilities.
Basic Inference
Section titled “Basic Inference”const UserSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email(), age: z.number().int().optional(),});
// Infer the TypeScript typetype User = z.infer<typeof UserSchema>;// Result: { id: string; name: string; email: string; age?: number | undefined }Input vs Output Types
Section titled “Input vs Output Types”Some schemas transform data (e.g., .default(), .transform()). Zod distinguishes input and output types.
const TransformSchema = z.object({ name: z.string().trim().toLowerCase(), // Transforms input tags: z.array(z.string()).default([]), // Adds default createdAt: z.string().datetime().transform((val) => new Date(val)), // String → Date});
type Input = z.input<typeof TransformSchema>;// { name: string; tags?: string[] | undefined; createdAt: string }
type Output = z.output<typeof TransformSchema>;// { name: string; tags: string[]; createdAt: Date }
// For most schemas without transforms, input and output are identicaltype Inferred = z.infer<typeof TransformSchema>; // Same as z.output<>Inferring from Procedures
Section titled “Inferring from Procedures”VeloxTS procedures automatically infer types from schemas.
const createUser = procedure() .input(z.object({ name: z.string(), email: z.string().email() })) .output(z.object({ id: z.string(), name: z.string(), email: z.string() })) .mutation(async ({ input, ctx }) => { // input: { name: string; email: string } - automatically typed return ctx.db.user.create({ data: input }); });
// Frontend client gets full type safetyimport { api } from '@/client';
const user = await api.users.createUser({ name: 'Alice', email: 'alice@example.com',});// user: { id: string; name: string; email: string }Common Patterns
Section titled “Common Patterns”Input/Output Schema Pairs
Section titled “Input/Output Schema Pairs”Typical pattern for CRUD operations.
// Base creation schemaexport const CreateUserSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), role: z.enum(['admin', 'editor', 'viewer']).default('viewer'),});
// Update schema - all fields optionalexport const UpdateUserSchema = CreateUserSchema.partial();
// Output schema - adds server-generated fieldsexport const UserSchema = CreateUserSchema.extend({ id: z.string().uuid(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(),});
// Filter/search schemaexport const UserFilterSchema = z.object({ role: z.enum(['admin', 'editor', 'viewer']).optional(), search: z.string().optional(), page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(100).default(20),});Reusable Schema Utilities
Section titled “Reusable Schema Utilities”VeloxTS provides common schema helpers.
import { idParamSchema, // { id: string } paginationInputSchema, // { page, limit, sortBy?, sortOrder? }} from '@veloxts/validation';
// Use in proceduresexport const userProcedures = procedures('users', { // Get by ID getUser: procedure() .input(idParamSchema) .output(UserSchema) .query(async ({ input, ctx }) => { return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }); }),
// Paginated list listUsers: procedure() .input(paginationInputSchema) .output(z.object({ data: z.array(UserSchema), pagination: z.object({ page: z.number(), limit: z.number(), total: z.number(), }), })) .query(async ({ input, ctx }) => { // input.page, input.limit already validated // ... }),});Sharing Schemas Across Stack
Section titled “Sharing Schemas Across Stack”Define schemas once, use everywhere.
import { z } from '@veloxts/velox';
export const CreateProductSchema = z.object({ name: z.string().min(1, 'Product name is required').max(200), price: z.number().positive('Price must be positive'), description: z.string().max(2000),});
export const ProductSchema = CreateProductSchema.extend({ id: z.string().uuid(), createdAt: z.string().datetime(),});
// Backend procedureimport { CreateProductSchema, ProductSchema } from '@shared/schemas/product';
export const productProcedures = procedures('products', { createProduct: procedure() .input(CreateProductSchema) .output(ProductSchema) .mutation(async ({ input, ctx }) => { return ctx.db.product.create({ data: input }); }),});
// Frontend form validationimport { CreateProductSchema } from '@shared/schemas/product';
function ProductForm() { const handleSubmit = (data: unknown) => { const result = CreateProductSchema.safeParse(data); if (!result.success) { // Show validation errors console.error(result.error.flatten()); return; }
// Submit validated data api.products.createProduct(result.data); };}Next Steps
Section titled “Next Steps”- Coercion - Automatic type conversion from strings
- Pagination - Pagination helpers and patterns
- Procedures - Using schemas in procedure definitions
Learn More
Section titled “Learn More”- Zod Official Documentation - Complete Zod reference
- Zod Error Handling - Advanced error customization
- Zod Coercion - Type coercion patterns