Zod Integration
Turn Zod validation errors into typed Results for seamless composition in workflows.
Why Combine Them?
Section titled “Why Combine Them?”- Type-safe validation errors — Know exactly what went wrong, not just “validation failed”
- Composable with other operations — Chain validation with database calls, API requests, etc.
- Early exit on invalid input —
step()stops the workflow immediately on validation failure
Quick Start
Section titled “Quick Start”import { z } from 'zod';import { ok, err, type Result } from 'awaitly';import { createWorkflow } from 'awaitly/workflow';
const UserSchema = z.object({ email: z.string().email(), age: z.number().min(18),});
type User = z.infer<typeof UserSchema>;type ValidationError = { type: 'VALIDATION'; issues: z.ZodIssue[] };
// Convert Zod's safeParse to a Resultconst zodToResult = <T>( schema: z.ZodSchema<T>, data: unknown): Result<T, ValidationError> => { const parsed = schema.safeParse(data); return parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION', issues: parsed.error.issues });};
// Use in a workflowconst workflow = createWorkflow('workflow', { validate: (data: unknown) => Promise.resolve(zodToResult(UserSchema, data)),});
const result = await workflow.run(async ({ step, deps }) => { const user = await step('validate', () => deps.validate({ email: 'test@example.com', age: 25 })); return user; // User type, not unknown});Patterns
Section titled “Patterns”Pattern 1: Basic Schema Validation
Section titled “Pattern 1: Basic Schema Validation”The simplest pattern—validate input and return a typed Result:
import { z } from 'zod';import { ok, err, type Result } from 'awaitly';
// Define your schemaconst CreateUserSchema = z.object({ email: z.string().email('Invalid email format'), password: z.string().min(8, 'Password must be at least 8 characters'), name: z.string().optional(),});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// Validation error with full issue detailstype ValidationError = { type: 'VALIDATION'; issues: z.ZodIssue[];};
// Generic helper (copy this to your utils)export const zodToResult = <T>( schema: z.ZodSchema<T>, data: unknown): Result<T, ValidationError> => { const parsed = schema.safeParse(data); return parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION', issues: parsed.error.issues });};
// Usageconst input = zodToResult(CreateUserSchema, requestBody);// input: Result<CreateUserInput, ValidationError>Pattern 2: In Workflows with step()
Section titled “Pattern 2: In Workflows with step()”Compose validation with other operations:
import { createWorkflow } from 'awaitly/workflow';import { zodToResult } from './utils';
const deps = { validateInput: (raw: unknown) => Promise.resolve(zodToResult(CreateUserSchema, raw)), saveToDatabase, sendWelcomeEmail,};
const createUserWorkflow = createWorkflow('workflow', deps);
const createUser = async (rawInput: unknown) => { return createUserWorkflow.run(async ({ step, deps }) => { // Validate input first — exits early if invalid const input = await step('validateInput', () => deps.validateInput(rawInput));
// Now input is typed as CreateUserInput const user = await step('saveToDatabase', () => deps.saveToDatabase(input));
await step('sendWelcomeEmail', () => deps.sendWelcomeEmail(user.email));
return user; });};
// Error type is automatically: ValidationError | DbError | EmailError | UnexpectedErrorPattern 3: Form Validation in React
Section titled “Pattern 3: Form Validation in React”Return validation results to forms:
import { z } from 'zod';import { zodToResult } from './utils';
const SignUpSchema = z.object({ email: z.string().email('Please enter a valid email'), password: z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain an uppercase letter') .regex(/[0-9]/, 'Password must contain a number'), confirmPassword: z.string(),}).refine((data) => data.password === data.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'],});
// In your form handlerconst handleSubmit = async (formData: FormData) => { const raw = Object.fromEntries(formData); const validation = zodToResult(SignUpSchema, raw);
if (!validation.ok) { // Convert Zod issues to field errors for your form library const fieldErrors = validation.error.issues.reduce((acc, issue) => { const field = issue.path.join('.'); acc[field] = issue.message; return acc; }, {} as Record<string, string>);
return { success: false, errors: fieldErrors }; }
// Proceed with valid data const result = await createUser(validation.value); return result.ok ? { success: true, user: result.value } : { success: false, error: result.error };};Pattern 4: API Request Validation
Section titled “Pattern 4: API Request Validation”Validate incoming API requests:
import { createWorkflow } from 'awaitly/workflow';import { zodToResult } from './utils';
// Define request schemasconst CreatePostSchema = z.object({ title: z.string().min(1).max(200), content: z.string().min(1), tags: z.array(z.string()).max(10).optional(),});
const deps = { validatePost: (raw: unknown) => Promise.resolve(zodToResult(CreatePostSchema, raw)), createPost,};
const workflow = createWorkflow('workflow', deps);
// API handlerexport const POST = async (request: Request) => { const body = await request.json();
const result = await workflow.run(async ({ step, deps }) => { const input = await step('validatePost', () => deps.validatePost(body)); const post = await step('createPost', () => deps.createPost(input)); return post; });
if (!result.ok) { if (result.error.type === 'VALIDATION') { return Response.json( { error: 'Validation failed', issues: result.error.issues }, { status: 400 } ); } return Response.json({ error: 'Server error' }, { status: 500 }); }
return Response.json(result.value, { status: 201 });};Pattern 5: Nested Schema Validation
Section titled “Pattern 5: Nested Schema Validation”Handle complex nested data:
const AddressSchema = z.object({ street: z.string(), city: z.string(), country: z.string(), postalCode: z.string(),});
const OrderSchema = z.object({ items: z.array(z.object({ productId: z.string().uuid(), quantity: z.number().int().positive(), })).min(1, 'Order must have at least one item'), shippingAddress: AddressSchema, billingAddress: AddressSchema.optional(),});
// Validate with detailed error pathsconst validateOrder = (data: unknown) => { const result = zodToResult(OrderSchema, data);
if (!result.ok) { // Issues include full paths like 'items.0.quantity' or 'shippingAddress.city' console.log(result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`)); }
return result;};Complete Example: User Registration
Section titled “Complete Example: User Registration”Complete workflow combining Zod validation with database operations:
import { z } from 'zod';import { ok, err, type Result, type AsyncResult } from 'awaitly';import { createWorkflow } from 'awaitly/workflow';
// Schemasconst EmailSchema = z.string().email();const PasswordSchema = z.string() .min(8) .regex(/[A-Z]/, 'Must contain uppercase') .regex(/[a-z]/, 'Must contain lowercase') .regex(/[0-9]/, 'Must contain number');
const RegisterSchema = z.object({ email: EmailSchema, password: PasswordSchema, name: z.string().min(2).max(100),});
// Error typestype ValidationError = { type: 'VALIDATION'; issues: z.ZodIssue[] };type EmailTakenError = { type: 'EMAIL_TAKEN'; email: string };type DbError = { type: 'DB_ERROR'; message: string };
// Dependenciesconst deps = { validateInput: (raw: unknown) => { const parsed = RegisterSchema.safeParse(raw); return Promise.resolve( parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION' as const, issues: parsed.error.issues }) ); },
checkEmailExists: async (email: string): AsyncResult<boolean, DbError> => { // In real code, this would query the database return ok(false); },
createUser: async (data: { email: string; password: string; name: string }): AsyncResult< { id: string; email: string; name: string }, DbError > => { return ok({ id: '123', email: data.email, name: data.name }); },};
// Registration workflowconst registerWorkflow = createWorkflow('workflow', deps);
const register = async (rawInput: unknown) => { return registerWorkflow.run(async ({ step, deps }) => { // Step 1: Validate input const input = await step('validateInput', () => deps.validateInput(rawInput));
// Step 2: Check if email is taken const emailExists = await step('checkEmailExists', () => deps.checkEmailExists(input.email)); if (emailExists) { return await step('createUser', err({ type: 'EMAIL_TAKEN' as const, email: input.email })); }
// Step 3: Create user const user = await step('createUser', () => deps.createUser(input));
return user; });};
// Usageconst result = await register({ email: 'user@example.com', password: 'SecurePass123', name: 'Jane Doe',});
if (!result.ok) { switch (result.error.type) { case 'VALIDATION': console.log('Invalid input:', result.error.issues); break; case 'EMAIL_TAKEN': console.log('Email already registered:', result.error.email); break; case 'DB_ERROR': console.log('Database error:', result.error.message); break; }}Common Utilities
Section titled “Common Utilities”Copy this utility file to your project:
import { z } from 'zod';import { ok, err, type Result } from 'awaitly';
export type ValidationError = { type: 'VALIDATION'; issues: z.ZodIssue[];};
/** * Convert Zod safeParse to Result */export const zodToResult = <T>( schema: z.ZodSchema<T>, data: unknown): Result<T, ValidationError> => { const parsed = schema.safeParse(data); return parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION', issues: parsed.error.issues });};
/** * Async version for schemas with async refinements */export const zodToResultAsync = async <T>( schema: z.ZodSchema<T>, data: unknown): Promise<Result<T, ValidationError>> => { const parsed = await schema.safeParseAsync(data); return parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION', issues: parsed.error.issues });};
/** * Convert Zod issues to a field error map (useful for forms) */export const issuesToFieldErrors = (issues: z.ZodIssue[]): Record<string, string> => { return issues.reduce((acc, issue) => { const field = issue.path.join('.') || '_root'; acc[field] = issue.message; return acc; }, {} as Record<string, string>);};
/** * Get the first error message (useful for simple error displays) */export const getFirstError = (error: ValidationError): string => { return error.issues[0]?.message ?? 'Validation failed';};Next Steps
Section titled “Next Steps”- Prisma Integration for database operations
- React Query Integration for client-side data fetching
- Workflows for composing validated operations