Express

Lesson 10

Avoiding business logic in routes

Keep route handlers thin by delegating business decisions to services.

Good Code

src/reviews/router.ts
router.post("/:id/approve", async (req, res) => {
  const review = await reviewWorkflow.approve({
    reviewId: req.params.id,
    approverId: res.locals.user.id,
  });

  res.json({ data: review });
});

Bad Code

src/reviews/router.ts
router.post("/:id/approve", async (req, res) => {
  const review = await db.review.findUnique({ where: { id: req.params.id } });

  if (!review || review.status !== "pending") {
    res.status(409).json({ error: "Review cannot be approved." });
    return;
  }

  await db.review.update({
    where: { id: req.params.id },
    data: { status: "approved", approvedBy: res.locals.user.id },
  });
  await notifications.sendApprovalEmail(review.authorId);

  res.json({ ok: true });
});

Review Notes

What to review

Good Code

The good version keeps the route focused on HTTP input and output while the workflow owns approval rules and side effects.

Bad Code

The bad version performs database reads, business checks, writes, notification side effects, and response formatting in one route.

Takeaways

  • Routes should translate HTTP to application calls, not contain the whole use case.