How to spot it
- The function name contains
and,also, orwith—fetchAndSaveUser,validateAndSendEmail - There are comment blocks inside acting as section headers —
// 1. Validate,// 2. Save - You can't write a single unit test without mocking half the app
- Changing one scenario forces you to edit the middle of a big function
A real example
handleJobApplication method you might find in a NestJS service:
// ❌ God Function — 5 responsibilities in one place
async function handleJobApplication(dto: ApplyJobDto, userId: string) {
// 1. Validate input
if (!dto.jobId || !dto.coverLetter) {
throw new BadRequestException('Missing fields');
}
// 2. Check for duplicate application
const existing = await this.applicationRepo.findOne({
where: { jobId: dto.jobId, userId },
});
if (existing) throw new ConflictException('Already applied');
// 3. Load resume
const resume = await this.fileService.getFile(dto.resumeFileId);
if (!resume) throw new NotFoundException('Resume not found');
// 4. Save application
const application = this.applicationRepo.create({
jobId: dto.jobId,
userId,
coverLetter: dto.coverLetter,
resumeUrl: resume.url,
status: ApplicationStatus.PENDING,
});
await this.applicationRepo.save(application);
// 5. Send confirmation email
const user = await this.userRepo.findOne({ where: { id: userId } });
await this.mailer.sendMail({
to: user.email,
subject: 'Application received',
template: 'job-application',
context: { name: user.name, jobId: dto.jobId },
});
return application;
}The refactored version
// ✅ Each function does one thing
async function handleJobApplication(dto: ApplyJobDto, userId: string) {
await this.validateApplication(dto, userId);
const resume = await this.getResumeOrThrow(dto.resumeFileId);
const application = await this.saveApplication(dto, userId, resume);
await this.notifyApplicant(userId, dto.jobId);
return application;
}
private async validateApplication(dto: ApplyJobDto, userId: string) {
if (!dto.jobId || !dto.coverLetter) {
throw new BadRequestException('Missing fields');
}
const existing = await this.applicationRepo.findOne({
where: { jobId: dto.jobId, userId },
});
if (existing) throw new ConflictException('Already applied');
}
private async getResumeOrThrow(fileId: string) {
const resume = await this.fileService.getFile(fileId);
if (!resume) throw new NotFoundException('Resume not found');
return resume;
}
private async saveApplication(
dto: ApplyJobDto,
userId: string,
resume: FileEntity,
) {
const application = this.applicationRepo.create({
jobId: dto.jobId,
userId,
coverLetter: dto.coverLetter,
resumeUrl: resume.url,
status: ApplicationStatus.PENDING,
});
return this.applicationRepo.save(application);
}
private async notifyApplicant(userId: string, jobId: string) {
const user = await this.userRepo.findOne({ where: { id: userId } });
await this.mailer.sendMail({
to: user.email,
subject: 'Application received',
template: 'job-application',
context: { name: user.name, jobId },
});
}What you actually gain
- Testing — each piece can be tested in complete isolation
- Reuse —
notifyApplicantcan now be called from other flows - Change safety — updating email logic only touches
notifyApplicant, nothing else - Readability —
handleJobApplicationreads like a summary of the process
The rule of thumb
// Step 3 inside a function body, stop. That comment is telling you something.