---
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 документы для инженерной команды
- Связанные документы:
  - [Индекс Agents](../README.md)
  - [Команды разработки](../commands.md)
  - [Инженерные принципы](../../governance/engineering-principles.md)

## 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):**

```typescript
@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):**

```typescript
// 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
