From Spaghetti to Clean: A Step-by-Step Refactoring
Walk through a complete refactoring of a messy real-world function, applying clean code principles one step at a time.
Let’s take a realistic, messy function and systematically refactor it into clean code. No artificial examples — this is the kind of code you’ll actually encounter in production codebases.
The Spaghetti
A user registration endpoint handler that grew organically over months:
app.post('/api/register', async (req, res) => {
try {
const { email, password, name, referralCode } = req.body;
// check
if (!email || !password || !name) {
return res.status(400).json({ error: 'Missing fields' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
return res.status(400).json({ error: 'Bad email' });
}
// check if exists
const existing = await db.query('SELECT id FROM users WHERE email = $1', [email.toLowerCase()]);
if (existing.rows.length > 0) {
return res.status(409).json({ error: 'Email taken' });
}
// hash pw
const salt = await bcrypt.genSalt(12);
const hashed = await bcrypt.hash(password, salt);
// save
const result = await db.query(
'INSERT INTO users (email, password_hash, name, created_at) VALUES ($1, $2, $3, NOW()) RETURNING *',
[email.toLowerCase(), hashed, name.trim()]
);
const user = result.rows[0];
// handle referral
if (referralCode) {
const referrer = await db.query('SELECT id FROM users WHERE referral_code = $1', [referralCode]);
if (referrer.rows.length > 0) {
await db.query('INSERT INTO referrals (referrer_id, referred_id) VALUES ($1, $2)', [referrer.rows[0].id, user.id]);
await db.query('UPDATE users SET credit = credit + 10 WHERE id = $1', [referrer.rows[0].id]);
await db.query('UPDATE users SET credit = credit + 5 WHERE id = $1', [user.id]);
}
}
// generate code for this user
const code = name.toLowerCase().replace(/[^a-z]/g, '').slice(0, 6) + Math.random().toString(36).slice(2, 6);
await db.query('UPDATE users SET referral_code = $1 WHERE id = $2', [code, user.id]);
// send email
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, { expiresIn: '24h' });
await transporter.sendMail({
from: '[email protected]',
to: email,
subject: 'Verify your email',
html: `<a href="https://app.com/verify?token=${token}">Click to verify</a>`,
});
// log
console.log(`New user: ${email}`);
await db.query('INSERT INTO audit_log (action, user_id, details) VALUES ($1, $2, $3)',
['user_registered', user.id, JSON.stringify({ email, referralCode })]);
res.status(201).json({ id: user.id, email: user.email, name: user.name });
} catch (err) {
console.error('Registration error:', err);
res.status(500).json({ error: 'Internal error' });
}
});
Step 1: Extract Validation
interface RegistrationData {
email: string;
password: string;
name: string;
referralCode?: string;
}
function validateRegistration(data: Partial<RegistrationData>): RegistrationData {
const { email, password, name, referralCode } = data;
if (!email || !password || !name) {
throw new ValidationError('Email, password, and name are required');
}
if (password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new ValidationError('Invalid email format');
}
return {
email: email.toLowerCase().trim(),
password,
name: name.trim(),
referralCode,
};
}
Step 2: Extract Repository
class UserRepository {
constructor(private db: Pool) {}
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
return result.rows[0] ?? null;
}
async create(data: { email: string; passwordHash: string; name: string }): Promise<User> {
const result = await this.db.query(
'INSERT INTO users (email, password_hash, name, created_at) VALUES ($1, $2, $3, NOW()) RETURNING *',
[data.email, data.passwordHash, data.name]
);
return result.rows[0];
}
async setReferralCode(userId: string, code: string): Promise<void> {
await this.db.query('UPDATE users SET referral_code = $1 WHERE id = $2', [code, userId]);
}
async addCredit(userId: string, amount: number): Promise<void> {
await this.db.query('UPDATE users SET credit = credit + $1 WHERE id = $2', [amount, userId]);
}
async findByReferralCode(code: string): Promise<User | null> {
const result = await this.db.query('SELECT * FROM users WHERE referral_code = $1', [code]);
return result.rows[0] ?? null;
}
}
Step 3: Extract Domain Logic
class RegistrationService {
constructor(
private userRepo: UserRepository,
private referralService: ReferralService,
private emailService: EmailService,
private auditLogger: AuditLogger,
) {}
async register(data: RegistrationData): Promise<User> {
await this.ensureEmailAvailable(data.email);
const passwordHash = await bcrypt.hash(data.password, 12);
const user = await this.userRepo.create({
email: data.email,
passwordHash,
name: data.name,
});
const referralCode = generateReferralCode(data.name);
await this.userRepo.setReferralCode(user.id, referralCode);
if (data.referralCode) {
await this.referralService.processReferral(data.referralCode, user.id);
}
await this.emailService.sendVerification(user);
await this.auditLogger.log('user_registered', user.id, { email: data.email });
return user;
}
private async ensureEmailAvailable(email: string): Promise<void> {
const existing = await this.userRepo.findByEmail(email);
if (existing) throw new ConflictError('Email already registered');
}
}
function generateReferralCode(name: string): string {
const prefix = name.toLowerCase().replace(/[^a-z]/g, '').slice(0, 6);
const suffix = Math.random().toString(36).slice(2, 6);
return `${prefix}${suffix}`;
}
Step 4: Clean Controller
app.post('/api/register', async (req, res, next) => {
try {
const data = validateRegistration(req.body);
const user = await registrationService.register(data);
res.status(201).json({
id: user.id,
email: user.email,
name: user.name,
});
} catch (error) {
next(error); // Let error middleware handle it
}
});
What Changed
| Aspect | Before | After |
|---|---|---|
| Lines in handler | 60+ | 8 |
| Responsibilities | 7+ | 1 (orchestration) |
| Testable units | 0 (all in one function) | 5+ (each class independently) |
| Database coupling | Direct SQL everywhere | Behind repository |
| Error handling | Inconsistent | Centralized middleware |
| Reusability | None | Each service reusable |
The same functionality, but now each piece is focused, testable, and maintainable. This is the power of systematic refactoring — small, safe steps that add up to a massive improvement.