Qadam Roadmap
проект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