# Controllers

## 1. The Controller's One Job

A controller's responsibility is narrow and specific: receive a parsed request, delegate to whatever business logic is needed, and shape the result into a response. It does **not** contain business logic itself. This is the thin-controller principle first introduced in `01-express_fundamentals/` Common Pitfall 5 and enforced structurally by NestJS's controller/provider split (`02-nestjs_fundamentals/` section 2) — this topic makes that principle explicit and shows what "thin" concretely looks like in practice.

```ts
// THICK controller -- business logic embedded directly in the handler
async function createOrder(req: Request, res: Response) {
  const { userId, items } = req.body;
  const user = await db.users.findById(userId);
  if (!user) return res.status(404).json({ error: "user not found" });
  let total = 0;
  for (const item of items) {
    const product = await db.products.findById(item.productId);
    total += product.price * item.quantity;
  }
  if (total > user.creditLimit) return res.status(422).json({ error: "over credit limit" });
  const order = await db.orders.create({ userId, items, total });
  await emailService.sendOrderConfirmation(user.email, order);
  res.status(201).json(order);
}

// THIN controller -- delegates every business decision to a service
async function createOrder(req: Request, res: Response) {
  try {
    const order = await ordersService.create(req.body.userId, req.body.items);
    res.status(201).json(order);
  } catch (err) {
    handleServiceError(err, res); // maps known error types to status codes -- 09-error_response_design/
  }
}
```

The thick version mixes at least four distinct concerns in one function: request parsing, data lookup, a business rule (credit limit check), order persistence, and a side effect (sending email) — none of which can be reused, tested in isolation, or reasoned about independently. The thin version's controller does exactly one thing: translate an HTTP request into a service call, and a service result into an HTTP response.

## 2. Why This Split Matters as an App Grows

A business rule embedded directly in a controller (like the credit-limit check above) can only ever run in response to an HTTP request — it can't be reused from a CLI script, a scheduled background job (`16-background_jobs/`), or a test that wants to check the rule in isolation without spinning up a full HTTP request/response cycle. Extracting it into a service (`10-services_and_repositories/` covers this split in depth) makes the logic callable from anywhere, and makes the controller itself trivial to test — a thin controller's test just checks "did it call the service correctly and map the result to the right response," not "did the business rule produce the right number."

![A thin controller extracts input, calls one service method and maps the result to HTTP, while business rules live in the service and data access in the repository](diagrams/01-thin-controller.svg)

## 3. What a Controller SHOULD Do

- Extract and lightly validate request data (delegating deeper validation to `08-validation_and_dtos/`'s DTO/schema layer, not hand-rolling it inline)
- Call exactly the service method(s) needed for this specific request
- Translate the service's result (or thrown error) into the correct HTTP status code and response shape (`09-error_response_design/`)
- Handle only HTTP-specific concerns: reading headers, setting cookies, choosing status codes — never business rules

## 4. What a Controller Should NOT Do

- Query a database directly (that's the service/repository layer's job)
- Contain conditional business rules (credit limits, eligibility checks, pricing logic)
- Call external services directly (payment providers, email senders) — those belong behind a service abstraction too, both for testability and so the controller doesn't need to know *how* the side effect happens
- Format data beyond simple response shaping — any real transformation belongs in the service layer or a dedicated mapper

## 5. One Controller, Multiple Related Routes

```ts
@Controller("orders")
class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Get()
  findAll() { return this.ordersService.findAll(); }

  @Get(":id")
  findOne(@Param("id") id: string) { return this.ordersService.findById(id); }

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

  @Delete(":id")
  cancel(@Param("id") id: string) { return this.ordersService.cancel(id); }
}
```

Grouping every route for one resource (`orders`) into one controller class is a direct consequence of the route-grouping/prefixing idea from `04-routing/` section 4 — the controller class itself IS the group, with the shared `@Controller("orders")` prefix applied once instead of repeated on every method.

## Common Pitfalls

1. **A conditional business rule living directly in a controller method** — makes the rule untestable in isolation and unreachable from any non-HTTP call site (section 2).
2. **A controller calling the database or an external API directly**, skipping the service layer entirely — even if it "works," it means the same data-access logic gets re-implemented in every controller that needs it, instead of written once in a service.
3. **A controller doing deep, multi-field validation inline** instead of delegating to a schema/DTO layer (`08-validation_and_dtos/`) — scatters validation rules across every handler instead of centralizing them.
4. **Treating "thin controller" as "no logic at all," including HTTP-specific logic that genuinely belongs there** — choosing a status code, reading a header, setting a cookie IS the controller's job; the principle is about business logic, not all logic whatsoever.

## Quick Reference

```
Controller's job: parse request -> call service -> shape response
Controller's NOT job: database queries, business rules, external API calls

Test signal: if a controller's test needs to mock a database, the controller is too thick
Reuse signal: if a rule can only run via HTTP, it's probably misplaced in the controller
Group related routes for one resource into one controller class (ties to 04-routing/ prefixing)
```
