Skip to content

[Refactoring] Services: Extract validation schemas from product.service.ts into shared validation module #280

@syed-reza98

Description

@syed-reza98

Problem

The product.service.ts file is currently 1,662 lines long, making it one of the largest files in the codebase. A significant portion (approximately 150+ lines) consists of Zod validation schemas that are tightly coupled with the service logic. Additionally, similar validation patterns for common fields (email, phone, URLs) are duplicated across 10+ files in the codebase.

Current Code Location

  • Primary file: src/lib/services/product.service.ts (lines 66-165)
  • Complexity: High (1,662 total lines, 150+ lines of validation schemas)
  • Related duplication: Email validation in 10+ files, phone validation in 10+ files

Proposed Refactoring

Extract Zod validation schemas into a centralized, reusable validation module. This will:

  1. Reduce the product service file size by ~150 lines
  2. Enable schema reuse across the codebase
  3. Standardize validation patterns for common fields (email, phone, URLs, addresses)
  4. Improve testability of validation logic in isolation

Benefits

  • Maintainability: Changes to validation rules happen in one place
  • Consistency: Standardized validation across all forms and services
  • Testability: Validation schemas can be tested independently
  • Reusability: Common schemas (email, phone, address) can be composed into larger schemas
  • Discoverability: Developers can find all validation rules in one location
  • Type Safety: Inferred types from schemas remain consistent across usage

Suggested Approach

  1. Create new module structure:

    src/lib/validation/
    ├── index.ts              # Barrel export
    ├── common.ts             # Common field validators (email, phone, url, etc.)
    ├── address.ts            # Address validation schemas
    ├── product.ts            # Product-specific schemas (from product.service.ts)
    ├── order.ts              # Order validation schemas
    └── store.ts              # Store validation schemas
  2. Extract common validators (src/lib/validation/common.ts):

    import { z } from 'zod';
    
    // Email validation - standardize across all usages
    export const emailSchema = z.string().email('Invalid email address');
    
    // Phone validation - consistent pattern across checkout, orders, customers
    export const phoneSchema = z.string()
      .regex(/^\+?[\d\s\-()]{10,}$/, 'Please enter a valid phone number')
      .min(10, 'Phone number must be at least 10 digits');
    
    // URL validation with relative path support
    export const urlSchema = z.string().refine((v) => {
      if (!v) return true;
      try {
        new URL(v);
        return true;
      } catch {
        return typeof v === 'string' && v.startsWith('/');
      }
    }, { message: 'Invalid URL' });
    
    // Optional string that allows empty string to be treated as undefined
    export const optionalString = z.string().optional().or(z.literal(""));
  3. Extract product schemas (src/lib/validation/product.ts):

    import { z } from 'zod';
    import { DiscountType, ProductStatus, InventoryStatus } from '`@prisma/client`';
    import { urlSchema, optionalString } from './common';
    
    export const variantSchema = z.object({
      id: z.string().cuid().optional(),
      name: z.string().min(1, "Variant name is required").max(255),
      sku: z.string().min(1, "Variant SKU is required").max(100),
      barcode: z.string().max(100).optional().nullable(),
      price: z.coerce.number().min(0).optional().nullable(),
      compareAtPrice: z.coerce.number().min(0).optional().nullable(),
      discountType: z.nativeEnum(DiscountType).optional().nullable().default(DiscountType.NONE),
      discountValue: z.coerce.number().min(0).optional().nullable(),
      discountStartDate: z.coerce.date().optional().nullable(),
      discountEndDate: z.coerce.date().optional().nullable(),
      inventoryQty: z.coerce.number().int().min(0).default(0),
      lowStockThreshold: z.coerce.number().int().min(0).default(5),
      weight: z.coerce.number().min(0).optional().nullable(),
      image: urlSchema.optional().nullable(),
      options: z.union([z.string(), z.record(z.string(), z.string())]).default("{}"),
      isDefault: z.boolean().default(false),
    });
    
    export const createProductSchema = z.object({
      // ... (extracted from product.service.ts lines 99-164)
    });
    
    export const updateProductSchema = createProductSchema.partial().extend({
      id: z.string().cuid(),
    });
    
    export type VariantData = z.infer(typeof variantSchema);
    export type CreateProductData = z.infer(typeof createProductSchema);
    export type UpdateProductData = z.infer(typeof updateProductSchema);
  4. Update product.service.ts to import schemas:

    import { 
      variantSchema, 
      createProductSchema, 
      updateProductSchema,
      type VariantData,
      type CreateProductData,
      type UpdateProductData
    } from '@/lib/validation/product';
  5. Migrate existing usages:

    • Update src/app/store/[slug]/checkout/page.tsx to use phoneSchema from common validators
    • Update src/components/checkout/shipping-details-step.tsx to use standardized validators
    • Update all email validation in 10+ files to use emailSchema
    • Update phone validation in 10+ files to use phoneSchema

Code Example

Before (scattered across multiple files):

// src/app/store/[slug]/checkout/page.tsx
const checkoutSchema = z.object({
  email: z.string().email("Please enter a valid email address"),
  phone: z.string().regex(/^\+?[\d\s\-()]{10,}$/, "Please enter a valid phone number"),
  // ...
});

// src/components/stores/store-form-dialog.tsx
const storeSchema = z.object({
  email: z.string().email('Invalid email address'),
  phone: z.string().optional(),
  // ...
});

// src/lib/services/product.service.ts (lines 66-165)
export const variantSchema = z.object({
  // 30+ lines of validation logic
});
export const createProductSchema = z.object({
  // 65+ lines of validation logic
});

After (centralized and reusable):

// src/lib/validation/common.ts
export const emailSchema = z.string().email('Invalid email address');
export const phoneSchema = z.string().regex(/^\+?[\d\s\-()]{10,}$/, 'Please enter a valid phone number');

// src/lib/validation/product.ts
export const variantSchema = z.object({ /* ... */ });
export const createProductSchema = z.object({ /* ... */ });

// Usage in checkout
import { emailSchema, phoneSchema } from '@/lib/validation/common';

const checkoutSchema = z.object({
  email: emailSchema,
  phone: phoneSchema,
  // ...
});

// Usage in product service
import { variantSchema, createProductSchema } from '@/lib/validation/product';

Impact Assessment

  • Effort: Medium - Estimated 4-6 hours

    • 1-2 hours: Create validation module structure
    • 2-3 hours: Extract and organize schemas
    • 1-2 hours: Update all imports and test
  • Risk: Low

    • No behavioral changes, only code organization
    • Validation logic remains identical
    • Type safety preserved through Zod inference
    • Can be done incrementally (start with common validators)
  • Benefit: High

    • Reduces product.service.ts by ~150 lines (9% reduction)
    • Eliminates 20+ instances of duplicated validation code
    • Standardizes validation patterns across entire codebase
    • Makes validation logic discoverable and reusable
  • Priority: High - This is a foundational improvement that will benefit many future features

Related Files

Files to create:

  • src/lib/validation/index.ts
  • src/lib/validation/common.ts
  • src/lib/validation/address.ts
  • src/lib/validation/product.ts

Files to update (import changes only):

  • src/lib/services/product.service.ts
  • src/app/store/[slug]/checkout/page.tsx
  • src/components/checkout/shipping-details-step.tsx
  • src/components/stores/store-form-dialog.tsx
  • src/components/admin/create-store-form.tsx
  • src/components/customers/customer-dialog.tsx
  • src/lib/services/store.service.ts
  • src/lib/services/order.service.ts
  • ...and 10+ other files with email/phone validation

Testing Strategy

  1. Unit tests for validators:

    // src/lib/validation/__tests__/common.test.ts
    describe('emailSchema', () => {
      it('accepts valid emails', () => {
        expect(emailSchema.parse('test@example.com')).toBe('test@example.com');
      });
      
      it('rejects invalid emails', () => {
        expect(() => emailSchema.parse('not-an-email')).toThrow();
      });
    });
  2. Existing integration tests should pass without modification (validation behavior unchanged)

  3. Type checking: Run npm run type-check to ensure all imports resolve correctly

  4. Manual testing: Test forms that use migrated validators (checkout, store creation, customer management)

Success Criteria

  • All validation schemas extracted to src/lib/validation/
  • Common validators (email, phone, URL) standardized across codebase
  • product.service.ts reduced by ~150 lines
  • All existing tests pass
  • No TypeScript errors
  • Documentation added to src/lib/validation/README.md explaining the module structure

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

  • expires on Mar 8, 2026, 1:41 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