8.8 KiB
id, title, category, priority, tags, related
| id | title | category | priority | tags | related | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| core-dry-extraction | DRY - Code Extraction | core-principles | critical |
|
|
DRY - Code Extraction
Don't Repeat Yourself. When you find duplicated code, extract it into a reusable function, method, or module. Every piece of knowledge should have a single, unambiguous representation.
Bad Example
// Anti-pattern: Same validation logic repeated in multiple places
class UserController {
async createUser(req: Request, res: Response): Promise<void> {
const { email, password, name } = req.body;
// Email validation - duplicated
if (!email) {
res.status(400).json({ error: 'Email is required' });
return;
}
if (!email.includes('@') || !email.includes('.')) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
if (email.length > 255) {
res.status(400).json({ error: 'Email too long' });
return;
}
// Password validation - duplicated
if (!password) {
res.status(400).json({ error: 'Password is required' });
return;
}
if (password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
if (!/[A-Z]/.test(password)) {
res.status(400).json({ error: 'Password must contain uppercase letter' });
return;
}
if (!/[0-9]/.test(password)) {
res.status(400).json({ error: 'Password must contain a number' });
return;
}
// Create user...
}
async updateUser(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
// Same email validation repeated
if (email) {
if (!email.includes('@') || !email.includes('.')) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
if (email.length > 255) {
res.status(400).json({ error: 'Email too long' });
return;
}
}
// Same password validation repeated
if (password) {
if (password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
if (!/[A-Z]/.test(password)) {
res.status(400).json({ error: 'Password must contain uppercase letter' });
return;
}
if (!/[0-9]/.test(password)) {
res.status(400).json({ error: 'Password must contain a number' });
return;
}
}
// Update user...
}
async resetPassword(req: Request, res: Response): Promise<void> {
const { email, newPassword } = req.body;
// Email validation repeated again
if (!email) {
res.status(400).json({ error: 'Email is required' });
return;
}
if (!email.includes('@') || !email.includes('.')) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
// Password validation repeated again
if (!newPassword) {
res.status(400).json({ error: 'New password is required' });
return;
}
if (newPassword.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
if (!/[A-Z]/.test(newPassword)) {
res.status(400).json({ error: 'Password must contain uppercase letter' });
return;
}
if (!/[0-9]/.test(newPassword)) {
res.status(400).json({ error: 'Password must contain a number' });
return;
}
// Reset password...
}
}
Good Example
// Correct approach: Extract reusable validation functions
// Validation result type
interface ValidationResult {
isValid: boolean;
errors: string[];
}
// Reusable validation functions
class Validators {
static email(email: string | undefined, options: { required?: boolean } = {}): ValidationResult {
const errors: string[] = [];
if (!email) {
if (options.required) {
errors.push('Email is required');
}
return { isValid: !options.required, errors };
}
if (!email.includes('@') || !email.includes('.')) {
errors.push('Invalid email format');
}
if (email.length > 255) {
errors.push('Email must be 255 characters or less');
}
return { isValid: errors.length === 0, errors };
}
static password(password: string | undefined, options: { required?: boolean } = {}): ValidationResult {
const errors: string[] = [];
if (!password) {
if (options.required) {
errors.push('Password is required');
}
return { isValid: !options.required, errors };
}
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
return { isValid: errors.length === 0, errors };
}
static combine(...results: ValidationResult[]): ValidationResult {
const errors = results.flatMap(r => r.errors);
return { isValid: errors.length === 0, errors };
}
}
// Reusable error response helper
function validationError(res: Response, errors: string[]): void {
res.status(400).json({ errors });
}
// Clean controller using extracted validations
class UserController {
async createUser(req: Request, res: Response): Promise<void> {
const { email, password, name } = req.body;
const validation = Validators.combine(
Validators.email(email, { required: true }),
Validators.password(password, { required: true })
);
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Create user...
}
async updateUser(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
const validation = Validators.combine(
Validators.email(email),
Validators.password(password)
);
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Update user...
}
async resetPassword(req: Request, res: Response): Promise<void> {
const { email, newPassword } = req.body;
const validation = Validators.combine(
Validators.email(email, { required: true }),
Validators.password(newPassword, { required: true })
);
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Reset password...
}
}
// Validators can be reused across the application
class AdminController {
async inviteUser(req: Request, res: Response): Promise<void> {
const { email } = req.body;
const validation = Validators.email(email, { required: true });
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Send invite...
}
}
// Easy to test in isolation
describe('Validators', () => {
describe('email', () => {
it('should reject invalid email format', () => {
const result = Validators.email('invalid');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Invalid email format');
});
it('should accept valid email', () => {
const result = Validators.email('user@example.com');
expect(result.isValid).toBe(true);
});
it('should require email when required option is set', () => {
const result = Validators.email(undefined, { required: true });
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Email is required');
});
});
describe('password', () => {
it('should reject short passwords', () => {
const result = Validators.password('short');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must be at least 8 characters');
});
it('should require uppercase letter', () => {
const result = Validators.password('lowercase123');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must contain at least one uppercase letter');
});
});
});
Why
-
Single Source of Truth: Password rules are defined once. Change them in one place, and all usages are updated.
-
Consistency: All email validations behave the same way. No risk of inconsistent error messages or rules.
-
Easier Testing: Test the validation logic once, thoroughly. No need to test the same logic in every controller.
-
Bug Fixes Propagate: Fix a bug in
Validators.email(), and it's fixed everywhere. -
Reduced Code Size: Less code means less to read, less to maintain, and fewer places for bugs.
-
Better Abstraction: Controllers focus on HTTP concerns, validators focus on validation.
-
Reusability: Same validators work in controllers, services, CLI tools, or anywhere else.