Node.js API Best Practices

Chapter 1 · Practice 2 of 4

Separate routes, business logic and data access

From Project structure and configuration

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

TypeScript
// 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));
});