Input Validation

Battle-tested patterns for secure input handling and validation using Zod.

Overview#

Input validation is your first line of defense against malicious data. This pattern covers:

  • Schema validation with Zod
  • Server action integration
  • Input sanitization
  • Environment variable validation

Prerequisites#

npm install zod isomorphic-dompurify

Code Example#

Zod Schema Validation#

1// lib/validations/user.ts 2import { z } from 'zod' 3 4export const CreateUserSchema = z.object({ 5 email: z.string().email('Invalid email address'), 6 name: z.string() 7 .min(1, 'Name is required') 8 .max(100, 'Name too long') 9 .regex(/^[a-zA-Z\s'-]+$/, 'Invalid characters in name'), 10 password: z.string() 11 .min(8, 'Password must be at least 8 characters') 12 .regex(/[A-Z]/, 'Password must contain uppercase letter') 13 .regex(/[a-z]/, 'Password must contain lowercase letter') 14 .regex(/[0-9]/, 'Password must contain number') 15 .regex(/[^A-Za-z0-9]/, 'Password must contain special character'), 16 age: z.number() 17 .int('Age must be a whole number') 18 .min(13, 'Must be at least 13 years old') 19 .max(120, 'Invalid age') 20 .optional() 21}) 22 23export const UpdateProfileSchema = z.object({ 24 name: z.string().min(1).max(100).optional(), 25 bio: z.string().max(500).optional(), 26 website: z.string().url().optional().or(z.literal('')) 27}) 28 29export type CreateUserInput = z.infer<typeof CreateUserSchema> 30export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>

Server Action with Validation#

1// actions/user.ts 2'use server' 3 4import { CreateUserSchema } from '@/lib/validations/user' 5import { prisma } from '@/lib/db' 6 7type ActionResult = 8 | { success: true; data: { id: string } } 9 | { success: false; error: string; fieldErrors?: Record<string, string[]> } 10 11export async function createUser(formData: FormData): Promise<ActionResult> { 12 // Parse and validate 13 const raw = { 14 email: formData.get('email'), 15 name: formData.get('name'), 16 password: formData.get('password'), 17 age: formData.get('age') ? Number(formData.get('age')) : undefined 18 } 19 20 const result = CreateUserSchema.safeParse(raw) 21 22 if (!result.success) { 23 return { 24 success: false, 25 error: 'Validation failed', 26 fieldErrors: result.error.flatten().fieldErrors 27 } 28 } 29 30 // Hash password before storing 31 const hashedPassword = await hashPassword(result.data.password) 32 33 const user = await prisma.user.create({ 34 data: { 35 ...result.data, 36 password: hashedPassword 37 } 38 }) 39 40 return { success: true, data: { id: user.id } } 41}

Input Sanitization#

1// lib/sanitize.ts 2import DOMPurify from 'isomorphic-dompurify' 3 4// Sanitize HTML content (for rich text) 5export function sanitizeHtml(dirty: string): string { 6 return DOMPurify.sanitize(dirty, { 7 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'], 8 ALLOWED_ATTR: ['href', 'target', 'rel'] 9 }) 10} 11 12// Strip all HTML (for plain text fields) 13export function stripHtml(input: string): string { 14 return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] }) 15} 16 17// URL validation and sanitization 18export function sanitizeUrl(url: string): string | null { 19 try { 20 const parsed = new URL(url) 21 // Only allow http/https 22 if (!['http:', 'https:'].includes(parsed.protocol)) { 23 return null 24 } 25 return parsed.toString() 26 } catch { 27 return null 28 } 29}

Environment Variable Validation#

1// lib/env.ts 2import { z } from 'zod' 3 4const envSchema = z.object({ 5 DATABASE_URL: z.string().url(), 6 STRIPE_SECRET_KEY: z.string().startsWith('sk_'), 7 STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'), 8 NEXT_PUBLIC_URL: z.string().url(), 9 NODE_ENV: z.enum(['development', 'production', 'test']) 10}) 11 12// Validate at build/startup time 13export const env = envSchema.parse(process.env)

Usage Instructions#

  1. Define schemas for all user input in a dedicated validations directory
  2. Use safeParse for graceful error handling
  3. Return field-level errors to the client for better UX
  4. Sanitize any HTML content before storage or display
  5. Validate environment variables at startup to fail fast

Best Practices#

  • Always validate on the server - Client-side validation is for UX only
  • Use strict typing - Leverage TypeScript with z.infer<> for type safety
  • Sanitize output - Even validated data should be escaped when rendered
  • Fail fast - Validate environment variables at startup, not runtime
  • Keep schemas DRY - Share validation logic between client and server
  • Test edge cases - Include malicious input in your test suite