/** * Server-Side Validation Example * * Demonstrates: * - Using the SAME Zod schema on server * - Single source of truth for validation * - Error mapping from server to client * - Type-safe validation on both sides */ import { z } from 'zod' /** * SHARED SCHEMA - Use this exact schema on both client and server * Define it in a shared file (e.g., schemas/user.ts) and import on both sides */ export const userRegistrationSchema = z.object({ username: z.string() .min(3, 'Username must be at least 3 characters') .max(20, 'Username must not exceed 20 characters') .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'), email: z.string() .email('Invalid email address'), password: z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[0-9]/, 'Password must contain at least one number'), age: z.number() .int('Age must be a whole number') .min(13, 'You must be at least 13 years old') .max(120, 'Invalid age'), }).refine((data) => { // Custom validation: check if username is blacklisted const blacklistedUsernames = ['admin', 'root', 'system'] return !blacklistedUsernames.includes(data.username.toLowerCase()) }, { message: 'This username is not allowed', path: ['username'], }) type UserRegistrationData = z.infer /** * SERVER-SIDE VALIDATION (Next.js API Route Example) */ export async function POST(request: Request) { try { const body = await request.json() // 1. Parse and validate with Zod const validatedData = userRegistrationSchema.parse(body) // 2. Additional server-only validation (database checks, etc.) const usernameExists = await checkUsernameExists(validatedData.username) if (usernameExists) { return Response.json( { success: false, errors: { username: 'Username is already taken', }, }, { status: 400 } ) } const emailExists = await checkEmailExists(validatedData.email) if (emailExists) { return Response.json( { success: false, errors: { email: 'Email is already registered', }, }, { status: 400 } ) } // 3. Proceed with registration const user = await createUser(validatedData) return Response.json({ success: true, user: { id: user.id, username: user.username, email: user.email, }, }) } catch (error) { // 4. Handle Zod validation errors if (error instanceof z.ZodError) { return Response.json( { success: false, errors: error.flatten().fieldErrors, }, { status: 400 } ) } // 5. Handle other errors console.error('Registration error:', error) return Response.json( { success: false, message: 'An unexpected error occurred', }, { status: 500 } ) } } /** * SERVER-SIDE VALIDATION (Node.js/Express Example) */ import express from 'express' const app = express() app.post('/api/register', async (req, res) => { try { // Parse and validate const validatedData = userRegistrationSchema.parse(req.body) // Server-only checks const usernameExists = await checkUsernameExists(validatedData.username) if (usernameExists) { return res.status(400).json({ success: false, errors: { username: 'Username is already taken', }, }) } // Create user const user = await createUser(validatedData) res.json({ success: true, user, }) } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ success: false, errors: error.flatten().fieldErrors, }) } console.error('Registration error:', error) res.status(500).json({ success: false, message: 'An unexpected error occurred', }) } }) /** * SERVER-SIDE VALIDATION (Cloudflare Workers + Hono Example) */ import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' const app = new Hono() app.post('/api/register', zValidator('json', userRegistrationSchema), async (c) => { // Data is already validated by zValidator middleware const validatedData = c.req.valid('json') // Server-only checks const usernameExists = await checkUsernameExists(validatedData.username) if (usernameExists) { return c.json( { success: false, errors: { username: 'Username is already taken', }, }, 400 ) } // Create user const user = await createUser(validatedData) return c.json({ success: true, user, }) }) /** * CLIENT-SIDE INTEGRATION WITH SERVER ERRORS */ import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' function RegistrationForm() { const { register, handleSubmit, setError, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(userRegistrationSchema), defaultValues: { username: '', email: '', password: '', age: 18, }, }) const onSubmit = async (data: UserRegistrationData) => { try { const response = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) const result = await response.json() if (!result.success) { // Map server errors to form fields if (result.errors) { Object.entries(result.errors).forEach(([field, message]) => { setError(field as keyof UserRegistrationData, { type: 'server', message: Array.isArray(message) ? message[0] : message as string, }) }) } else { // Generic error setError('root', { type: 'server', message: result.message || 'Registration failed', }) } return } // Success - redirect or show success message console.log('Registration successful:', result.user) } catch (error) { setError('root', { type: 'server', message: 'Network error. Please try again.', }) } } return (
{errors.root && (
{errors.root.message}
)} {/* Form fields */} {errors.username && {errors.username.message}} {errors.email && {errors.email.message}} {errors.password && {errors.password.message}} {errors.age && {errors.age.message}}
) } /** * Helper functions (implement according to your database) */ async function checkUsernameExists(username: string): Promise { // Database query return false } async function checkEmailExists(email: string): Promise { // Database query return false } async function createUser(data: UserRegistrationData) { // Create user in database return { id: '1', ...data } } /** * KEY BENEFITS OF SERVER-SIDE VALIDATION: * * 1. Security - Client validation can be bypassed, server validation cannot * 2. Single Source of Truth - Same schema on client and server * 3. Type Safety - TypeScript types automatically inferred from schema * 4. Consistency - Same validation rules applied everywhere * 5. Database Checks - Server can validate against database (unique username, etc.) */