Good Code
The good version keeps the route focused on HTTP input and output while the workflow owns approval rules and side effects.
Lesson 10
Keep route handlers thin by delegating business decisions to services.
router.post("/:id/approve", async (req, res) => {
// The route only translates HTTP data into a workflow call.
const review = await reviewWorkflow.approve({
reviewId: req.params.id,
approverId: res.locals.user.id,
});
res.json({ data: review });
});router.post("/:id/approve", async (req, res) => {
// Database writes, rules, and notifications are hidden here.
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 });
});The good version keeps the route focused on HTTP input and output while the workflow owns approval rules and side effects.
The bad version performs database reads, business checks, writes, notification side effects, and response formatting in one route.