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-dompurifyCode 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#
- Define schemas for all user input in a dedicated validations directory
- Use
safeParsefor graceful error handling - Return field-level errors to the client for better UX
- Sanitize any HTML content before storage or display
- 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
Related Patterns#
- CSRF Protection - Protect against cross-site request forgery
- Security Headers - HTTP security headers
- Server Actions - Server action patterns
- Forms - Form handling with react-hook-form