Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Move business logic out of controllers into services
Controllers should orchestrate requests, not compute domain decisions. Push fetch-plus-condition logic into a service method.
Bad example
| 1 | @Controller('orders') |
| 2 | export class OrderController { |
| 3 | @Get(':id/summary') |
| 4 | async getSummary(@Param('id') id: number, @User() user: AuthUser) { |
| 5 | const order = await this.orderService.findById(id); |
| 6 |
|
| 7 | // Business logic computed inline in the controller |
| 8 | const [shipments, { isPremium }, returnCount] = await Promise.all([ |
| 9 | this.shipmentService.getShipments(order.items), |
| 10 | this.accountService.fetchUserTier(user.session), |
| 11 | this.returnService.countByUser(user.id), |
| 12 | ]); |
| 13 |
|
| 14 | const canRequestRefund = isPremium || returnCount === 0; |
| 15 |
|
| 16 | return { order, shipments, canRequestRefund }; |
| 17 | } |
| 18 | } |
Explanation (EN)
The controller fetches multiple sources and then derives the `canRequestRefund` domain decision inline. This couples HTTP routing to business rules, makes the rule hard to unit test without spinning up the controller, and prevents reuse by other callers.
Objašnjenje (HR)
Kontroler dohvaća više izvora podataka i zatim inline izvodi domensku odluku `canRequestRefund`. To veže HTTP rutiranje uz poslovna pravila, otežava jedinično testiranje pravila bez podizanja kontrolera i sprječava ponovnu upotrebu iz drugih pozivatelja.
Good example
| 1 | @Controller('orders') |
| 2 | export class OrderController { |
| 3 | @Get(':id/summary') |
| 4 | async getSummary(@Param('id') id: number, @User() user: AuthUser) { |
| 5 | return this.orderService.getSummary(id, user); |
| 6 | } |
| 7 | } |
| 8 |
|
| 9 | @Injectable() |
| 10 | export class OrderService { |
| 11 | async getSummary(id: number, user: AuthUser) { |
| 12 | const order = await this.findById(id); |
| 13 |
|
| 14 | const [shipments, { isPremium }, returnCount] = await Promise.all([ |
| 15 | this.shipmentService.getShipments(order.items), |
| 16 | this.accountService.fetchUserTier(user.session), |
| 17 | this.returnService.countByUser(user.id), |
| 18 | ]); |
| 19 |
|
| 20 | const canRequestRefund = isPremium || returnCount === 0; |
| 21 |
|
| 22 | return { order, shipments, canRequestRefund }; |
| 23 | } |
| 24 | } |
Explanation (EN)
The fetch-plus-condition logic lives in a service method. The controller is reduced to a single delegating call, the `canRequestRefund` rule is unit-testable in isolation, and the logic can be reused by any other entry point (CLI, job, another endpoint).
Objašnjenje (HR)
Logika dohvata i uvjeta sada živi u metodi servisa. Kontroler se svodi na jedan delegirajući poziv, pravilo `canRequestRefund` može se jedinično testirati izolirano, a logika se može ponovno upotrijebiti iz bilo koje druge ulazne točke (CLI, job, drugi endpoint).
Notes (EN)
Applies to any layered backend (NestJS, Express controllers, route handlers). A controller that does more than parse input, call one service, and shape the response is usually hiding business logic that belongs in a service. Promise.all-style parallel fetching is fine — just do it inside the service.
Bilješke (HR)
Vrijedi za svaki slojeviti backend (NestJS, Express kontroleri, route handleri). Kontroler koji radi više od parsiranja ulaza, poziva jednog servisa i oblikovanja odgovora obično skriva poslovnu logiku koja pripada servisu. Paralelno dohvaćanje u stilu Promise.all je u redu — samo ga obavi unutar servisa.
Exceptions / Tradeoffs (EN)
Trivial pass-through transformations (e.g. mapping a service DTO to a response shape) can stay in the controller. The rule targets domain decisions and multi-source orchestration, not simple formatting.
Iznimke / Tradeoffi (HR)
Trivijalne pass-through transformacije (npr. mapiranje servisnog DTO-a u oblik odgovora) mogu ostati u kontroleru. Pravilo cilja domenske odluke i orkestraciju više izvora, a ne jednostavno formatiranje.