Validation Guide
This guide covers the unified validation system for Elite Events.
Overview
All validation in the application uses a single, consolidated validation layer located at @/lib/validation. This provides:
- Type-safe validation with Zod schemas
- Consistent error formats across API and forms
- Reusable schemas for common patterns
- Middleware for API route validation
Quick Start
Importing
// Import schemas and utilities from single location
import {
// Common schemas
emailSchema,
passwordSchema,
paginationSchema,
// Domain schemas
loginSchema,
signupSchema,
createProductSchema,
checkoutSchema,
// Error utilities
formatZodError,
ValidationError,
} from '@/lib/validation';
// Import middleware separately (uses NextRequest, not available in Jest)
import { withValidation } from '@/lib/validation/middleware';
API Route Validation
Using withValidation Middleware
The recommended approach for API routes:
import { NextResponse } from 'next/server';
import { createProductSchema, paginationSchema } from '@/lib/validation';
import { withValidation } from '@/lib/validation/middleware';
import { z } from 'zod';
// POST with body validation
export const POST = withValidation(
{ body: createProductSchema },
async (request, { body }) => {
// body is fully typed as CreateProductInput
const product = await createProduct(body);
return NextResponse.json({ success: true, data: product });
}
);
// GET with query validation
export const GET = withValidation(
{ query: paginationSchema },
async (request, { query }) => {
const { page, limit, sortBy, sortOrder } = query;
const products = await getProducts({ page, limit, sortBy, sortOrder });
return NextResponse.json({ success: true, data: products });
}
);
// With route params
export const PUT = withValidation(
{
params: z.object({ id: z.coerce.number() }),
body: updateProductSchema,
},
async (request, { params, body }) => {
const product = await updateProduct(params.id, body);
return NextResponse.json({ success: true, data: product });
}
);
Manual Validation
For cases where middleware doesn't fit:
import { validateBody, validateQuery } from '@/lib/validation';
export async function POST(request: NextRequest) {
const result = await validateBody(request, createProductSchema);
if (!result.success) {
return result.response; // Returns 400 with validation errors
}
const data = result.data; // Fully typed
// ... handle request
}
Form Validation
Using useForm Hook
The useForm hook in @/lib/forms/useForm provides comprehensive form management:
'use client';
import { useForm } from '@/lib/forms/useForm';
import { signupSchema } from '@/lib/validation';
function SignupForm() {
const {
values,
errors,
touched,
isValid,
isSubmitting,
setValue,
setTouched,
handleSubmit,
} = useForm({
schema: signupSchema,
initialValues: {
email: '',
password: '',
confirmPassword: '',
firstName: '',
lastName: '',
acceptTerms: false,
},
});
const onSubmit = handleSubmit(async (data) => {
await signUp(data);
});
return (
<form onSubmit={onSubmit}>
<input
value={values.email}
onChange={(e) => setValue('email', e.target.value)}
onBlur={() => setTouched('email', true)}
/>
{touched.email && errors.email && (
<span className="text-red-500">{errors.email}</span>
)}
{/* ... other fields */}
</form>
);
}
Available Schemas
Common Schemas
| Schema | Description |
|---|---|
emailSchema | Email with validation and normalization |
passwordSchema | Password with complexity requirements |
simplePasswordSchema | Password without complexity (for login) |
nameSchema | Names (2-100 characters) |
phoneSchema | Phone number validation |
urlSchema | URL validation |
slugSchema | URL-friendly slugs |
idSchema | Positive integer IDs |
uuidSchema | UUID strings |
paginationSchema | page, limit, sortBy, sortOrder |
addressSchema | Street, city, state, zip, country |
Auth Schemas
| Schema | Description |
|---|---|
loginSchema | Email + password + rememberMe |
signupSchema | Full signup with confirmPassword |
forgotPasswordSchema | Email for password reset |
resetPasswordSchema | Token + new password |
changePasswordSchema | Current + new password |
User Schemas
| Schema | Description |
|---|---|
updateProfileSchema | Profile field updates |
createAddressSchema | New address creation |
notificationPreferencesSchema | Email/push/SMS prefs |
Product Schemas
| Schema | Description |
|---|---|
createProductSchema | Full product creation |
updateProductSchema | Partial product update |
productQuerySchema | Product list filters |
createReviewSchema | Product review |
updateInventorySchema | Stock adjustments |
Order Schemas
| Schema | Description |
|---|---|
addToCartSchema | Add item to cart |
checkoutSchema | Full checkout data |
updateOrderStatusSchema | Order status changes |
createPaymentSchema | Payment intent creation |
applyPromoCodeSchema | Promo code application |
Creating Custom Schemas
Extending Existing Schemas
import { z } from 'zod';
import { addressSchema, emailSchema } from '@/lib/validation';
// Extend with additional fields
const shippingAddressSchema = addressSchema.extend({
name: z.string().min(2),
phone: z.string().optional(),
instructions: z.string().max(500).optional(),
});
// Make fields optional
const partialAddressSchema = addressSchema.partial();
// Pick specific fields
const cityStateSchema = addressSchema.pick({ city: true, state: true });
Composing Schemas
import { z } from 'zod';
import { emailSchema, nameSchema } from '@/lib/validation';
const contactFormSchema = z.object({
name: nameSchema,
email: emailSchema,
subject: z.string().min(5).max(100),
message: z.string().min(20).max(2000),
category: z.enum(['general', 'support', 'sales', 'feedback']),
});
Error Handling
Error Types
interface ValidationError {
field: string; // e.g., "email" or "address.city"
message: string; // Human-readable message
code: string; // Zod error code
}
Formatting Errors
import { formatZodError, formatZodErrorToFieldMap } from '@/lib/validation';
try {
schema.parse(data);
} catch (error) {
if (error instanceof ZodError) {
// Array of { field, message, code }
const errors = formatZodError(error);
// Object of { [field]: message }
const fieldErrors = formatZodErrorToFieldMap(error);
}
}
Best Practices
- Import schemas from
@/lib/validation- Single source of truth - Import middleware from
@/lib/validation/middleware- For API routes - Use
withValidationfor API routes - Consistent error handling - Use
useFormfor forms - Built-in debouncing and state management - Extend existing schemas - Don't duplicate validation logic
- Use
.optional()for optional fields - Be explicit about nullability - Add custom error messages - Improve user experience
Migration from Old Systems
If you're updating code using older validation patterns:
// OLD - Don't use
import { validators } from '@/lib/core/validators';
import { withRequestValidation } from '@/lib/security/request-validator';
// NEW - Use this
import { emailSchema } from '@/lib/validation';
import { withValidation } from '@/lib/validation/middleware';
File Structure
src/lib/validation/
├── index.ts # Main entry point (re-exports all)
├── schemas.ts # Common primitive schemas
├── errors.ts # Error types and utilities
├── middleware.ts # withValidation HOC
├── api-validation.ts # Legacy API validation (still supported)
├── schemas/
│ ├── index.ts # Barrel export
│ ├── common.ts # Re-exports from schemas.ts
│ ├── auth.ts # Authentication schemas
│ ├── user.ts # User/profile schemas
│ ├── product.ts # Product/category schemas
│ └── order.ts # Order/cart/checkout schemas
└── __tests__/ # Validation tests