Module 43: Backend Architecture
Controllers and services
🏠The everyday picture
A restaurant's waiter (controller) takes the order and brings the food, but doesn't cook. The chef (service) cooks by the recipe, without ever talking to diners.
📘What it really is
- A controller translates between HTTP and the service:
// src/routes/enquiries.ts
enquiriesRouter.patch("/:id/close", async (req, res) => {
const input = closeSchema.parse(req.body); // Lesson 4
const enquiry = await enquiryService.close(req.params.id, input.note, req.user);
res.json(enquiry);
});
- A service holds the business rules and knows nothing about HTTP (no
reqorres):
// src/services/enquiryService.ts
export async function close(id: string, note: string, user: User): Promise<Enquiry> {
const enquiry = await enquiryRepo.findById(id);
if (!enquiry) throw new NotFoundError("Enquiry not found");
if (enquiry.status === "closed") throw new ConflictError("Enquiry is already closed");
const updated = { ...enquiry, status: "closed" as const, closedBy: user.id, closedNote: note };
return enquiryRepo.save(updated);
}
- Services throw meaningful errors (Lesson 5), which the error handler turns into status codes.
- Because services are plain functions, they can be tested directly, and reused by other entry points (a scheduled job, a command-line tool).
🖱️Try it
Create services/enquiryService.ts with functions for list, get, create, update status and delete. Move all rules
into it. Make each route handler short: read input, call one service function, respond.
✅Quick check
Should a service function receive req and res?
Show the answer
No. Services know nothing about HTTP; controllers pass them plain values.
Why are services easier to test than route handlers?
Show the answer
They're plain functions with no HTTP, so tests call them directly with values.
