-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
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+ timesz.coerce.number().int().min(0).default(0)- repeated 5+ timesz.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
- Create validation library at
src/lib/validation/schemas.ts - Extract common patterns into reusable schema builders
- Update service files to import shared schemas
- 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
- Create comprehensive unit tests for each validation schema
- Test edge cases: empty strings, null, undefined, malformed data
- Test error messages are user-friendly and consistent
- Verify backward compatibility - existing validation behavior unchanged
- Add integration tests verifying services use shared schemas correctly
- 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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Type
Projects
Status
Backlog