проектdocs/Agents/rules/api-thin-controllers.md
api-thin-controllers.md
Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев
title: Keep Controllers Thin — Business Logic in Services impact: CRITICAL impactDescription: Prevents duplicate logic and ensures testability tags: api, nestjs, controllers, services, architecture
Паспорт документа
- Статус документа: living standard
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении инженерной практики, CI/CD, архитектурных правил или локального workflow
- Область применения: внутренние rule/reference-card документы для инженерной команды
- Связанные документы:
Keep Controllers Thin — Business Logic in Services
Impact: CRITICAL
Controllers handle only HTTP concerns: parsing requests, calling services, formatting responses. All business logic lives in services.
Incorrect (fat controller):
@Controller('items')
export class ItemController {
constructor(private readonly prisma: PrismaService) {}
@Post()
async create(@Body() body: any, @Req() req: Request) {
// Validation in controller — wrong!
if (!body.title || body.title.length < 3) {
throw new BadRequestException('Title too short');
}
// Business logic in controller — wrong!
const slug = body.title.toLowerCase().replace(/\s+/g, '-');
const existing = await this.prisma.item.findFirst({ where: { slug } });
if (existing) {
throw new ConflictException('Slug already taken');
}
// Database access in controller — wrong!
const item = await this.prisma.item.create({
data: { ...body, slug, sellerId: req.user.sellerId, status: 'DRAFT' },
});
return item;
}
}
Correct (thin controller, fat service):
// item.controller.ts
@Controller('items')
export class ItemController {
constructor(private readonly itemService: ItemService) {}
@Post()
@UseGuards(JwtAuthGuard, SellerGuard)
async create(
@Body(new ZodValidationPipe(CreateItemSchema)) dto: CreateItemDTO,
@CurrentUser() user: AuthUser,
) {
return this.itemService.create(dto, user.sellerId);
}
}
// item.service.ts
@Injectable()
export class ItemService {
constructor(private readonly itemRepo: ItemRepository) {}
async create(dto: CreateItemDTO, sellerId: string): Promise<ItemDetailDTO> {
const slug = this.generateSlug(dto.title);
await this.ensureSlugUnique(slug);
return this.itemRepo.create({
...dto,
slug,
sellerId,
status: 'DRAFT',
});
}
private generateSlug(title: string): string {
return title.toLowerCase().replace(/\s+/g, '-');
}
private async ensureSlugUnique(slug: string): Promise<void> {
const existing = await this.itemRepo.findBySlug(slug);
if (existing) {
throw new DomainException(ErrorCode.SLUG_TAKEN, `Slug "${slug}" already exists`);
}
}
}
Controller responsibilities:
- Parse and validate request (via pipes/decorators)
- Call the appropriate service method
- Return the response (service already shapes it)
- Handle HTTP-specific concerns (status codes, headers)
Service responsibilities:
- All business logic and validation
- Orchestration between repositories
- Cache management
- Error throwing with domain exceptions