Skip to content

[Refactoring] Forms: Extract validation schemas into shared utility module #246

@syed-reza98

Description

@syed-reza98

Problem

Form validation logic is duplicated across multiple components with very similar Zod schemas. Specifically:

  1. Product validation appears in multiple places:

    • src/lib/services/product.service.ts (lines 66-163): Server-side createProductSchema
    • src/components/product-form.tsx (lines 52-75): Client-side productFormSchema
    • These schemas have ~70% overlap but slight differences
  2. Address validation is duplicated:

    • src/lib/services/order.service.ts (lines 83-93): addressSchema
    • src/app/store/[slug]/checkout/page.tsx (lines 56-100): Inline checkout validation
    • Both validate shipping/billing addresses with similar fields
  3. Common field validators are repeated across 10+ forms:

    • Email validation: z.string().email("Please enter a valid email")
    • Phone validation: z.string().regex(/^\+?[\d\s\-()]{10,}$/)
    • SKU validation: z.string().min(1).max(100)
    • Price validation: z.coerce.number().min(0)

Impact:

  • Inconsistency: Client and server validations diverge over time
  • Maintenance burden: Schema changes require updates in multiple files
  • Error-prone: Easy to miss validation rules when adding new forms
  • Poor DX: Developers must hunt for validation patterns to copy
  • Type safety issues: Schema types are redefined instead of imported

Current Code Location

  • Files affected:

    • src/lib/services/product.service.ts (product validation)
    • src/components/product-form.tsx (duplicate product validation)
    • src/lib/services/order.service.ts (address validation)
    • src/app/store/[slug]/checkout/page.tsx (827 lines, duplicate address validation)
    • Additional form components with inline schemas
  • Complexity: Medium-High

    • Affects both client and server code
    • Requires careful schema merging and testing

Proposed Refactoring

Create a centralized validation utility module that exports reusable Zod schemas and field validators, ensuring consistency between client and server validation.

Benefits

  • Single source of truth: One schema definition for client and server
  • Type safety: Import schema types instead of redefining them
  • Consistency: Guarantee identical validation across the app
  • DRY principle: Reuse common field validators (email, phone, price)
  • Easier updates: Change validation rules in one place
  • Better testing: Test schemas once, use everywhere

Suggested Approach

  1. Create shared validation module: src/lib/validation/schemas.ts

    • Export common field validators (email, phone, price, etc.)
    • Export entity schemas (product, address, customer, etc.)
    • Export schema types for TypeScript
  2. Separate concerns where needed:

    • Base schemas for server-side (complete validation)
    • Derived schemas for client-side forms (may be partial)
    • Use Zod's .pick(), .omit(), .partial() for variants
  3. Refactor existing forms:

    • Import schemas instead of defining inline
    • Remove duplicate validation logic
    • Update type imports
  4. Add comprehensive tests: src/lib/validation/schemas.test.ts

Code Example

Before (current duplication):

// src/lib/services/product.service.ts (lines 99-163)
export const createProductSchema = z.object({
  name: z.string().min(1, "Product name is required").max(255),
  slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Invalid slug format").optional(),
  description: z.string().optional(),
  price: z.coerce.number().min(0, "Price must be non-negative"),
  sku: z.string().min(1, "SKU is required").max(100),
  // ... 60+ more lines
});

// src/components/product-form.tsx (lines 52-75) - DUPLICATE!
const productFormSchema = z.object({
  name: z.string().min(1, 'Product name is required').max(255, 'Name too long'),
  slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Slug must be lowercase...').optional(),
  description: z.string().max(5000, 'Description too long').optional(),
  sku: z.string().min(1, 'SKU is required').max(100, 'SKU too long'),
  price: z.coerce.number().min(0, 'Price must be non-negative'),
  // Similar but not identical...
});

// src/app/store/[slug]/checkout/page.tsx (lines 56-68)
const checkoutSchema = z.object({
  email: z.string().email("Please enter a valid email address"),
  firstName: z.string().min(1, "First name is required"),
  lastName: z.string().min(1, "Last name is required"),
  phone: z.string().regex(/^\+?[\d\s\-()]{10,}$/, "Please enter a valid phone..."),
  // ... address fields
});

After (with shared validation):

// src/lib/validation/common-fields.ts
export const validators = {
  email: () => z.string().email("Please enter a valid email address"),
  phone: () => z.string().regex(/^\+?[\d\s\-()]{10,}$/, "Please enter a valid phone number"),
  price: () => z.coerce.number().min(0, "Price must be non-negative"),
  sku: () => z.string().min(1, "SKU is required").max(100),
  slug: () => z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Invalid slug format").optional(),
  name: (maxLength = 255) => z.string().min(1, "Name is required").max(maxLength),
};

// src/lib/validation/schemas.ts
import { validators } from './common-fields';

// Complete server-side product schema
export const productSchema = z.object({
  name: validators.name(255),
  slug: validators.slug(),
  description: z.string().optional(),
  price: validators.price(),
  compareAtPrice: validators.price().optional().nullable(),
  sku: validators.sku(),
  // ... other fields with consistent validation
});

// Client-side form schema (derived from base)
export const productFormSchema = productSchema.pick({
  name: true,
  slug: true,
  description: true,
  price: true,
  sku: true,
  // ... pick only fields needed for form
});

export const createProductSchema = productSchema; // For API
export const updateProductSchema = productSchema.partial().extend({
  id: z.string().cuid(),
});

// Address schema (reusable)
export const addressSchema = z.object({
  firstName: validators.name(100),
  lastName: validators.name(100),
  email: validators.email(),
  phone: validators.phone(),
  address: z.string().min(5, "Address is required"),
  city: z.string().min(1, "City is required"),
  state: z.string().min(1, "State/Province is required"),
  postalCode: z.string().min(3, "Postal code is required"),
  country: z.string().min(2, "Country is required"),
});

// Checkout schema uses address schema
export const checkoutSchema = z.object({
  email: validators.email(),
  firstName: validators.name(100),
  lastName: validators.name(100),
  phone: validators.phone(),
  // Flatten or nest address
  shippingAddress: addressSchema.omit({ email: true, firstName: true, lastName: true }),
  billingAddress: addressSchema.omit({ email: true, firstName: true, lastName: true }).optional(),
  billingSameAsShipping: z.boolean().default(true),
  paymentMethod: z.enum(["CASH_ON_DELIVERY", "CREDIT_CARD"]),
  discountCode: z.string().optional(),
});

// Export types
export type Product = z.infer(typeof productSchema);
export type ProductFormData = z.infer(typeof productFormSchema);
export type Address = z.infer(typeof addressSchema);
export type CheckoutFormData = z.infer(typeof checkoutSchema);

// src/lib/services/product.service.ts (refactored)
import { productSchema, createProductSchema, updateProductSchema } from '@/lib/validation/schemas';

export class ProductService {
  async createProduct(storeId: string, data: z.infer(typeof createProductSchema)) {
    const validatedData = createProductSchema.parse(data); // Same schema everywhere!
    // ...
  }
}

// src/components/product-form.tsx (refactored)
import { productFormSchema } from '@/lib/validation/schemas';
import type { ProductFormData } from '@/lib/validation/schemas';

export function ProductForm() {
  const form = useForm(ProductFormData)({
    resolver: zodResolver(productFormSchema), // Reuse server schema
    // ...
  });
}

Impact Assessment

  • Effort: Medium (2-3 days)

    • Day 1: Create validation module, migrate product and address schemas
    • Day 2: Refactor forms to use shared schemas
    • Day 3: Update tests, verify validation consistency
  • Risk: Low

    • Changes are type-safe (TypeScript will catch issues)
    • Existing validation logic preserved, just relocated
    • Can validate forms still work correctly
  • Benefit: Very High

    • Eliminates 15+ duplicate validation definitions
    • Prevents client-server validation drift
    • Significantly improves developer experience
    • Makes adding new forms much faster
  • Priority: High

Related Files

  • src/lib/services/product.service.ts
  • src/lib/services/order.service.ts
  • src/components/product-form.tsx (857 lines)
  • src/components/product-edit-form.tsx (886 lines)
  • src/app/store/[slug]/checkout/page.tsx (827 lines)
  • Additional forms with inline schemas

Testing Strategy

  1. Unit tests for validation schemas:

    • Test valid inputs pass validation
    • Test invalid inputs fail with correct messages
    • Test edge cases (empty strings, null, undefined)
  2. Integration tests:

    • Verify forms submit successfully with valid data
    • Verify forms show correct error messages
    • Test server-side validation matches client-side
  3. Type safety tests:

    • Compile-time type checking
    • Ensure exported types match schema inference

Migration Checklist

  • Create src/lib/validation/ directory
  • Extract common field validators
  • Create base schemas (product, address, order, etc.)
  • Add comprehensive tests
  • Migrate ProductService to use shared schemas
  • Migrate OrderService to use shared schemas
  • Refactor product-form.tsx
  • Refactor checkout/page.tsx
  • Update remaining forms
  • Remove old inline schema definitions
  • Update documentation

AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring

  • expires on Mar 1, 2026, 1:42 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions