Single Responsibility

Thin Controllers, Focused Services

Each class should have one reason to change.

LayerDoesDoes NOT
ControllerRead params/body, call a service, return the resultSQL, price calculation, sending email
ServiceBusiness rules (stock check, discount, order total)Know about HTTP request/response objects
RepositoryRead and write the databaseBusiness rules

Bad vs Good

Bad: the controller does everything.

@Controller('orders')
export class OrdersController {
  @Post()
  async placeOrder(@Body() dto: CreateOrderDto) {
    const product = await this.db.query('SELECT * FROM products WHERE id = $1', [dto.productId]);
    if (product.stock < dto.quantity) throw new BadRequestException('Out of stock');
    const total = product.price * dto.quantity * 1.18;
    await this.db.query('INSERT INTO orders ...');
    await this.mailer.send(dto.email, 'Order placed');
    return { total };
  }
}

Good: the controller delegates, the service orchestrates smaller services.

// orders.controller.ts
@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Post()
  placeOrder(@Body() dto: CreateOrderDto) {
    return this.ordersService.place(dto);
  }
}

// orders.service.ts
@Injectable()
export class OrdersService {
  constructor(
    private readonly products: ProductsService,
    private readonly pricing: PricingService,
    private readonly ordersRepo: OrdersRepository,
    private readonly notifications: NotificationsService,
  ) {}

  async place(dto: CreateOrderDto) {
    const product = await this.products.reserveStock(dto.productId, dto.quantity);
    const total = this.pricing.orderTotal(product.price, dto.quantity);
    const order = await this.ordersRepo.save({ ...dto, total });
    await this.notifications.orderPlaced(order);
    return order;
  }
}