Layer Two: The Service Layer
The service layer is where your product actually lives. Everything above it is transport and everything below it is storage.
The rule
A service module must not import anything from express, and must not know
what an HTTP status code is.
That one rule buys you a lot. It means you can call the same function from an HTTP route, a CLI script, a queue consumer or a test, and get the same behaviour. It also means the layer is testable without spinning up a server.
// services/progress-service/src/services/progress.service.js
async function completeLesson({ userId, lessonId }) {
const enrolment = await enrolmentRepository.findByUserAndLesson(userId, lessonId);
if (!enrolment) {
throw new NotFoundError('You are not enrolled in this course');
}
await lessonProgressRepository.markComplete({ enrolmentId: enrolment.id, lessonId });
await enrolmentRepository.refreshRollup(enrolment.id);
return enrolmentRepository.findById(enrolment.id);
}
NotFoundError is a domain error, not an HTTP one. A single error-handling
middleware maps it to 404 at the edge. If you ever call this from a queue
consumer, the same error means "give up, do not retry" - and that mapping
happens in the consumer, not in here.
What does not belong here
- SQL. It goes in a repository. If you find yourself importing a database driver into a service file, stop.
- Request parsing. By the time you reach this layer, the input is validated and shaped.
- Response formatting. Return domain objects. The controller decides what the wire format looks like.
Orchestration across services
When a service needs data another service owns, it makes an HTTP call, and that
call goes through a thin client in src/clients/. Keeping it there means the
retry policy, the timeout and the request-id header are written once:
const course = await courseClient.getCourse(courseId, { requestId });
If courseClient throws, you decide here whether the whole operation fails or
whether you can degrade. That decision is business logic, which is why it lives
in the service layer and nowhere else.