Skip to content

[Refactoring] Services: Extract shared Zod validation schemas into centralized validation library #227

@syed-reza98

Description

@syed-reza98

Problem

Zod validation schemas are duplicated across multiple service files, leading to:

  • Inconsistent validation rules for common fields (email, URL, price, quantity)
  • Code duplication with repeated patterns like z.coerce.number().min(0).optional().nullable()
  • Maintenance burden - changing validation logic requires updates in multiple places
  • Increased bundle size from repeated schema definitions

Examples of Duplication

Email validation appears in at least 2 files:

  • src/lib/services/order.service.ts:86: z.string().email()
  • src/lib/services/store.service.ts:18: z.string().email()

Price/numeric validation appears 20+ times across services:

  • z.coerce.number().min(0).optional().nullable() - repeated 10+ times
  • z.coerce.number().int().min(0).default(0) - repeated 5+ times
  • z.coerce.number().min(0, "Price must be non-negative") - various forms

URL validation appears in multiple files:

  • z.string().url().optional() - repeated in brand, category, store services
  • Custom URL/path validation in product service (lines 81-92, 128-137)

Current Code Location

  • Files affected:
    • src/lib/services/product.service.ts (lines 66-163)
    • src/lib/services/brand.service.ts (lines 41-42)
    • src/lib/services/category.service.ts (line 54)
    • src/lib/services/order.service.ts (line 86)
    • src/lib/services/store.service.ts (lines 17-18, 20)
  • Pattern frequency: 30+ repeated validation patterns across services

Proposed Refactoring

Benefits

  • Single source of truth for validation rules
  • Consistency across all services
  • Easier updates - change validation logic in one place
  • Better error messages - standardized, user-friendly messages
  • Type safety - shared types from validation schemas
  • Reduced bundle size - schema reuse instead of duplication

Suggested Approach

  1. Create validation library at src/lib/validation/schemas.ts
  2. Extract common patterns into reusable schema builders
  3. Update service files to import shared schemas
  4. Create comprehensive tests for validation library

Code Example

Before:

// product.service.ts
export const createProductSchema = z.object({
  price: z.coerce.number().min(0, "Price must be non-negative"),
  compareAtPrice: z.coerce.number().min(0).optional().nullable(),
  inventoryQty: z.coerce.number().int().min(0).default(0),
  email: z.string().email(),
  thumbnailUrl: z.string().url().optional().nullable(),
  // ... more fields
});

// order.service.ts
export const orderSchema = z.object({
  email: z.string().email(),
  totalPrice: z.coerce.number().min(0),
  // ... different implementation, same concepts
});

// store.service.ts
export const storeSchema = z.object({
  email: z.string().email(),
  website: z.string().url().optional(),
  // ... yet another variation
});

After:

// src/lib/validation/schemas.ts
import { z } from 'zod';

/**
 * Common validation schemas for reuse across the application
 */

// Email validation
export const emailSchema = z
  .string()
  .email("Please enter a valid email address")
  .max(255)
  .toLowerCase()
  .trim();

export const optionalEmailSchema = emailSchema.optional().nullable();

// URL validation
export const urlSchema = z
  .string()
  .url("Please enter a valid URL")
  .max(2048);

export const optionalUrlSchema = urlSchema.optional().nullable();

// Image URL validation (supports both absolute URLs and relative paths)
export const imageUrlSchema = z.string().min(1).refine((v) => {
  try {
    new URL(v);
    return true;
  } catch {
    return typeof v === 'string' && v.startsWith('/');
  }
}, { message: 'Invalid image URL (must be absolute URL or path starting with /)' });

export const optionalImageUrlSchema = imageUrlSchema.optional().nullable();

// Numeric validation
export const positiveNumberSchema = z.coerce
  .number()
  .min(0, "Must be a positive number");

export const optionalPositiveNumberSchema = positiveNumberSchema.optional().nullable();

export const priceSchema = z.coerce
  .number()
  .min(0, "Price must be non-negative")
  .finite("Price must be a valid number");

export const optionalPriceSchema = priceSchema.optional().nullable();

export const quantitySchema = z.coerce
  .number()
  .int("Quantity must be a whole number")
  .min(0, "Quantity cannot be negative");

export const optionalQuantitySchema = quantitySchema.optional().nullable();

export const percentageSchema = z.coerce
  .number()
  .min(0, "Percentage must be between 0 and 100")
  .max(100, "Percentage must be between 0 and 100");

export const optionalPercentageSchema = percentageSchema.optional().nullable();

// String validation
export const slugSchema = z
  .string()
  .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Invalid slug format (use lowercase letters, numbers, and hyphens)")
  .max(255);

export const skuSchema = z
  .string()
  .min(1, "SKU is required")
  .max(100)
  .trim()
  .toUpperCase();

export const cuidSchema = z.string().cuid("Invalid ID format");

export const optionalCuidSchema = cuidSchema.optional().nullable();

// Phone validation
export const phoneSchema = z
  .string()
  .regex(/^\+?[\d\s\-()]{10,}$/, "Please enter a valid phone number (e.g., +1 555-123-4567)");

export const optionalPhoneSchema = phoneSchema.optional().nullable();

// Date validation
export const futureDateSchema = z.coerce
  .date()
  .refine((date) => date > new Date(), { message: "Date must be in the future" });

export const optionalFutureDateSchema = futureDateSchema.optional().nullable();

/**
 * Validation utilities
 */

// Validate percentage discount doesn't exceed 100
export const validatePercentageDiscount = (discountType: string, discountValue?: number | null) => {
  if (discountType === 'PERCENTAGE' && discountValue && discountValue > 100) {
    return false;
  }
  return true;
};

// Create a schema with custom error messages
export const createNumberRangeSchema = (min: number, max: number, fieldName: string) => {
  return z.coerce
    .number()
    .min(min, `${fieldName} must be at least ${min}`)
    .max(max, `${fieldName} cannot exceed ${max}`);
};

// src/lib/validation/types.ts
import { z } from 'zod';
import * as schemas from './schemas';

/**
 * Inferred types from common schemas for reuse
 */
export type Email = z.infer(typeof schemas.emailSchema);
export type Url = z.infer(typeof schemas.urlSchema);
export type Price = z.infer(typeof schemas.priceSchema);
export type Quantity = z.infer(typeof schemas.quantitySchema);
export type Slug = z.infer(typeof schemas.slugSchema);
export type SKU = z.infer(typeof schemas.skuSchema);
export type CUID = z.infer(typeof schemas.cuidSchema);

// Updated service files
// product.service.ts
import {
  priceSchema,
  optionalPriceSchema,
  quantitySchema,
  imageUrlSchema,
  optionalImageUrlSchema,
  slugSchema,
  skuSchema,
  optionalCuidSchema,
  percentageSchema,
} from '@/lib/validation/schemas';

export const createProductSchema = z.object({
  price: priceSchema,
  compareAtPrice: optionalPriceSchema,
  inventoryQty: quantitySchema.default(0),
  slug: slugSchema.optional(),
  sku: skuSchema,
  thumbnailUrl: optionalImageUrlSchema,
  categoryId: optionalCuidSchema,
  images: z.array(imageUrlSchema).default([]),
  // ... cleaner, more maintainable
});

// order.service.ts
import { emailSchema, priceSchema } from '@/lib/validation/schemas';

export const orderSchema = z.object({
  email: emailSchema,
  totalPrice: priceSchema,
  // ... consistent with other services
});

// store.service.ts
import { emailSchema, optionalUrlSchema } from '@/lib/validation/schemas';

export const storeSchema = z.object({
  email: emailSchema,
  website: optionalUrlSchema,
  // ... same validation rules as everywhere else
});

Impact Assessment

  • Effort: Medium - Estimated 1-2 days to extract and refactor
  • Risk: Low - Validation logic is well-defined and can be tested independently
  • Benefit: High - Significantly improves consistency and maintainability
  • Priority: High - Affects multiple critical services

Related Files

  • src/lib/services/product.service.ts (1662 lines)
  • src/lib/services/order.service.ts (863 lines)
  • src/lib/services/brand.service.ts (446 lines)
  • src/lib/services/category.service.ts (739 lines)
  • src/lib/services/store.service.ts (329 lines)
  • src/app/store/[slug]/checkout/page.tsx (1039 lines - uses similar patterns)

Testing Strategy

  1. Create comprehensive unit tests for each validation schema
  2. Test edge cases: empty strings, null, undefined, malformed data
  3. Test error messages are user-friendly and consistent
  4. Verify backward compatibility - existing validation behavior unchanged
  5. Add integration tests verifying services use shared schemas correctly
  6. Test type inference - ensure TypeScript types are correctly inferred from schemas

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

  • expires on Feb 27, 2026, 8:37 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