Chapter 1 · Practice 2 of 4
Separate routes, business logic and data access
Why it matters
Putting database queries, validation and business rules directly inside route handlers works for a demo, but it becomes hard to test and hard to change. Split the code into three layers:
- Routes or controllers parse the request and send the response.
- Services hold the business rules.
- A data layer talks to the database.
The key rule: only the route layer knows about req and res. Services take plain values and return plain values, so you can call them from a queue worker, a script or a test without HTTP.
Group these files by feature, such as orders/ and users/, rather than by type (controllers/, models/). Everything for one feature then lives in one place.
Example
// orders/routes.ts
router.post('/orders', async (req, res) => {
const input = createOrderSchema.parse(req.body);
const order = await orderService.create(req.user.id, input);
res.status(201).json(toOrderDto(order));
});